module NSWTopo::ArcGIS::Map

def pages(per_page)

def pages(per_page)
  objectid_field = @layer["fields"].find do |field|
    field["type"] == "esriFieldTypeOID"
  end&.fetch("name")
  raise "ArcGIS layer does not support dynamic layers: #{@name}" unless @service["supportsDynamicLayers"]
  raise "ArcGIS layer does not support SVG output: #{@name}" unless @service["supportedImageFormatTypes"].split(?,).include? "SVG"
  raise "ArcGIS layer does not have an objectid field: #{@name}" unless objectid_field
  @unique ||= @type_field
  @unique ||= @layer["fields"].find do |field|
    field.values_at("name", "alias").map(&:downcase).include? @layer.dig("drawingInfo", "renderer", "field1")&.downcase
  end&.fetch("name")
  @unique ||= @coded_values.min_by do |name, lookup|
    lookup.length
  end&.first
  raise NoUniqueFieldError unless @unique
  @count = classify(@unique).sum(&:last)
  return [GeoJSON::Collection.new(projection: projection, name: @name)].each if @count.zero?
  @fields ||= @layer["fields"].select do |field|
    Layer::FIELD_TYPES === field["type"]
  end.map do |field|
    field["name"]
  end
  include_objectid = @fields.include? objectid_field
  min, chunk, table = 0, 10000, {}
  loop do
    break unless table.length < @count
    page, where = {}, ["#{objectid_field}>=#{min}", "#{objectid_field}<#{min + chunk}", *@where]
    Set[*@fields, *extra_field].delete(objectid_field).each_slice(2) do |fields|
      classify(objectid_field, *fields, where: where).each do |attributes, count|
        objectid = attributes.delete objectid_field
        page[objectid] ||= include_objectid ? { objectid_field => objectid } : {}
        page[objectid].merge! attributes
      end
    end
  rescue Connection::Error
    (chunk /= 2) > 0 ? retry : raise
  else
    table.merge! page
    min += chunk
  end
  parent = @layer
  scale = loop do
    break parent["minScale"] if parent["minScale"]&.nonzero?
    break parent["effectiveMinScale"] if parent["effectiveMinScale"]&.nonzero?
    break unless parent_id = parent.dig("parentLayer", "id")
    parent = get_json parent_id
  end || begin
    case @service["units"]
    when "esriMeters" then 100000
    else raise "can't get features from layer: #{@name}"
    end
  end
  bounds = @layer["extent"].values_at("xmin", "xmax", "ymin", "ymax").each_slice(2)
  cx, cy = bounds.map { |bound| 0.5 * bound.sum }
  bbox, size = %W[#{cx},#{cy},#{cx},#{cy} #{TILE},#{TILE}]
  dpi = bounds.map { |b0, b1| 0.0254 * TILE * scale / (b1 - b0) }.min * 0.999
  renderer = case @geometry_type
  when "esriGeometryPoint"
    { type: "simple", symbol: { color: [0,0,0,255], size: 1, type: "esriSMS", style: "esriSMSSquare" } }
  when "esriGeometryPolyline"
    { type: "simple", symbol: { color: [0,0,0,255], width: 1, type: "esriSLS", style: "esriSLSSolid" } }
  when "esriGeometryPolygon"
    { type: "simple", symbol: { color: [0,0,0,255], width: 0, type: "esriSFS", style: "esriSFSSolid" } }
  else
    raise "unsupported ArcGIS geometry type: #{@geometry_type}"
  end
  dynamic_layer = { source: { type: "mapLayer", mapLayerId: @id }, drawingInfo: { showLabels: false, renderer: renderer } }
  sets = table.group_by(&:last).map(&:last).sort_by(&:length)
  Enumerator::Lazy.new(sets) do |yielder, objectids_properties|
    while objectids_properties.any?
      begin
        objectids, properties = objectids_properties.take(per_page).transpose
        dynamic_layers = [dynamic_layer.merge(definitionExpression: "#{objectid_field} IN (#{objectids.join ?,})")]
        export = get_json "export", format: "svg", dynamicLayers: dynamic_layers.to_json, bbox: bbox, size: size, mapScale: scale, dpi: dpi
        href = URI.parse export["href"]
        xml = Connection.new(href).get(href.path, &:body)
        xmin, xmax, ymin, ymax = export["extent"].values_at "xmin", "xmax", "ymin", "ymax"
      rescue Connection::Error
        (per_page /= 2) > 0 ? retry : raise
      end
      REXML::Document.new(xml).elements.collect("svg//g[@transform]//g[@transform][path[@d]]") do |group|
        a, b, c, d, e, f = group.attributes["transform"].match(/matrix\((.*)\)/)[1].split(?\s).map(&:to_f)
        coords = []
        group.elements["path[@d]"].attributes["d"].gsub(/\ *([MmZzLlHhVvCcSsQqTtAa])\ */) do
          ?\s + $1 + ?\s
        end.strip.split(?\s).slice_before(/[MmZzLlHhVvCcSsQqTtAa]/).each do |command, *numbers|
          raise "can't handle SVG path data command: #{command}" unless numbers.length.even?
          coordinates = numbers.each_slice(2).map do |x, y|
            fx, fy = [(a * Float(x) + c * Float(y) + e) / TILE, (b * Float(x) + d * Float(y) + f) / TILE]
            [fx * xmax + (1 - fx) * xmin, fy * ymin + (1 - fy) * ymax]
          end
          case command
          when ?Z then next
          when ?M then coords << coordinates
          when ?L then coords.last.concat coordinates
          when ?C
            coordinates.each_slice(3) do |points|
              raise "unexpected SVG response (bad path data)" unless points.length == 3
              curves = [[coords.last.last, *points]]
              while curve = curves.shift
                next if curve.first == curve.last
                curve_length = curve.each_cons(2).sum do |p0, p1|
                  (p1 - p0).norm
                end
                if (curve.first - curve.last).norm < 0.99 * curve_length
                  reduced = 3.times.inject [ curve ] do |reduced|
                    reduced << reduced.last.each_cons(2).map do |p0, p1|
                      (p0 + p1) / 2
                    end
                  end
                  curves.unshift reduced.map(&:last).reverse
                  curves.unshift reduced.map(&:first)
                else
                  coords.last << curve.last
                end
              end
            end
          else raise "can't handle SVG path data command: #{command}"
          end
        end
        coords
      end.tap do |coords|
        lengths = [properties.length, coords.length]
        raise "unexpected SVG response (expected %i features, received %i)" % lengths if lengths.inject(&:<)
      end.zip(properties).map do |coords, properties|
        case @geometry_type
        when "esriGeometryPoint"
          raise "unexpected SVG response (bad point symbol)" unless coords.map(&:length) == [ 4 ]
          point = coords[0].transpose.map { |coords| coords.sum / coords.length }
          next GeoJSON::Point[point, properties]
        when "esriGeometryPolyline"
          next GeoJSON::LineString[coords[0], properties] if @mixed && coords.one?
          next GeoJSON::MultiLineString[coords, properties]
        when "esriGeometryPolygon"
          polys = GeoJSON::MultiLineString[coords.map(&:reverse), properties].to_multipolygon
          next @mixed && polys.one? ? polys.first : polys
        end
      end.tap do |features|
        yielder << GeoJSON::Collection.new(projection: projection, name: @name, features: features)
      end
      objectids_properties.shift per_page
    end
  end
end