module NSWTopo::VectorRender

def categorise(string)

def categorise(string)
  string.tr_s('^_a-zA-Z0-9', ?-).delete_prefix(?-).delete_suffix(?-)
end

def create

def create
  @features = get_features.reproject_to(@map.neatline.projection).clip(@map.neatline(**MARGIN))
  @map.write filename, @features.to_json
end

def drawing_features

def drawing_features
  features.explode.reject do |feature|
    feature["draw"] == false
  end
end

def features

def features
  @features ||= GeoJSON::Collection.load @map.read(filename)
end

def filename

def filename
  "#{@name}.json"
end

def labeling_features

def labeling_features
  features.select do |feature|
    feature["label"]
  end
end

def params_for(categories)

def params_for(categories)
  params.select do |key, value|
    Array(key).any? do |selector|
      String(selector).split(?\s).to_set <= categories
    end
  end.values.inject(params, &:deep_merge)
end

def render(knockout:, **, &block)

def render(knockout:, **, &block)
  defs = REXML::Element.new("defs").tap(&block)
  defs.add_attributes "id" => "#{@name}.defs"
  drawing_features.group_by do |feature, categories|
    categories || Array(feature["category"]).map(&:to_s).map(&method(:categorise)).to_set
  end.flat_map do |categories, features|
    dupes = params_for(categories)["dupe"]
    Array(dupes).map(&:to_s).map do |dupe|
      [categories | Set[dupe], [name, *categories, "content"].join(?.)]
    end.push [categories, features]
  end.tap do |ordered|
    params.fetch("order", []).reverse.map(&:split).map(&:to_set).each do |filter|
      ordered.sort_by!.with_index do |(categories, features), index|
        [filter <= categories ? 0 : 1, index]
      end
    end
  end.each do |categories, features|
    ids = [name, *categories]
    use = REXML::Element.new("use")
    use.add_attributes "id" => ids.join(?.)
    case features
    when String
      use.add_attributes "href" => "##{features}"
    when Array
      content = defs.add_element "g", "id" => [*ids, "content"].join(?.)
      use.add_attributes "href" => "#" + [*ids, "content"].join(?.)
    end
    use.tap(&block)
    category_params = params_for(categories)
    font_size, stroke_width, bezier = category_params.values_at "font-size", "stroke-width", "bezier"
    subdivide = category_params.slice("subdivide", "section").values.first
    category_params.slice(*SVG_ATTRIBUTES).tap do |svg_attributes|
      svg_attributes.slice(*FONT_SCALED_ATTRIBUTES).each do |key, value|
        svg_attributes[key] = svg_attributes[key].to_i * font_size * 0.01 if /^\d+%$/ === value
      end if font_size
      use.add_attributes svg_attributes
    end
    features.each do |feature, _|
      case feature
      when GeoJSON::Point
        symbol_id = [*ids, "symbol"].join(?.)
        transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*feature.coordinates, feature.fetch("rotation", @map.rotation) - @map.rotation]
        content.add_element "use", "transform" => transform, "href" => "#%s" % symbol_id
      when GeoJSON::LineString
        (subdivide ? feature.subdivide(subdivide) : feature).explode.each do |feature|
          content.add_element "path", "fill" => "none", "d" => feature.svg_path_data(bezier: bezier)
        end
      when GeoJSON::Polygon
        content.add_element "path", "fill-rule" => "nonzero", "d" => feature.svg_path_data
      when REXML::Element
        case feature.name
        when "text", "textPath" then content << feature
        when "path" then defs << feature
        end
      end
    end if content
    category_params.each do |command, args|
      next unless args
      args = args.map(&:to_a).inject([], &:+) if Array === args && args.all?(Hash)
      case command
      when "blur"
        filter_id = [*ids, "blur"].join(?.)
        use.add_attribute "filter", "url(#%s)" % filter_id
        defs.add_element("filter", "id" => filter_id).add_element "feGaussianBlur", "stdDeviation" => args, "in" => "SourceGraphic"
      when "symbol"
        next unless content
        symbol = defs.add_element "g", "id" => [*ids, "symbol"].join(?.)
        args.each do |element, attributes|
          if attributes
            symbol.add_element element, attributes
          else
            symbol.add_element REXML::Document.new(element).root
          end
        end
      when "pattern"
        dimensions, args = args.partition do |key, value|
          %w[width height].include? key
        end
        width, height = Hash[dimensions].values_at "width", "height"
        pattern_id = [*ids, "pattern"].join(?.)
        pattern = defs.add_element "pattern", "id" => pattern_id, "patternUnits" => "userSpaceOnUse", "width" => width, "height" => height
        args.each do |element, attributes|
          pattern.add_element element, attributes
        end
        use.add_attribute "fill", "url(#%s)" % pattern_id
      when "symbolise"
        next unless content
        args, symbols = args.partition do |element, attributes|
          %w[interval offset].include? element
        end
        interval, offset = Hash[args].values_at "interval", "offset"
        symbol_ids = symbols.map.with_index do |(element, attributes), index|
          symbol_id = [*ids, "symbol", index].join(?.).tap do |symbol_id|
            defs.add_element("g", "id" => symbol_id).add_element(element, attributes)
          end
        end
        rings = features.grep(GeoJSON::Polygon).map(&:rings).flat_map(&:explode)
        lines = features.grep(GeoJSON::LineString)
        (rings + lines).each do |feature|
          feature.sample_at(interval, offset: offset) do |point, along, angle|
            "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*point, 180.0 * angle / Math::PI]
          end.each do |transform|
            content.add_element "use", "transform" => transform, "href" => "#%s" % symbol_ids.sample
          end
        end
      when "inpoint", "outpoint", "endpoint"
        next unless content
        symbol_id = [*ids, command].join(?.)
        symbol = defs.add_element "g", "id" => symbol_id
        args.each do |element, attributes|
          symbol.add_element element, attributes
        end
        features.grep(GeoJSON::LineString).map(&:coordinates).each do |line|
          case command
          when "inpoint"  then [line.first(2)]
          when "outpoint" then [line.last(2).rotate]
          when "endpoint" then [line.first(2), line.last(2).rotate]
          end.each do |p0, p1|
            transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*p0, 180.0 * (p1 - p0).angle / Math::PI]
            use.add_element "use", "transform" => transform, "href" => "#%s" % symbol_id
          end
        end
      when "knockout"
        use.add_attributes "mask" => "url(##{knockout})"
        Knockout.new(use, *args).tap(&block)
      when "preserve"
        use.add_attributes "mask" => "none"
      when "cutout", "mask"   # mask deprecated
        Cutout.new(use).tap(&block)
      when "barrier", "fence" # fence deprecated
        next unless content && args
        buffer = 0.5 * (Numeric === args ? args : Numeric === stroke_width ? stroke_width : 0)
        features.grep_v(REXML::Element).each do |feature|
          Labels::ConvexHulls.new(feature, buffer).tap(&block)
        end
      when "shield"
        next unless content
        content.elements.each("text") do |element|
          case
          when text_length = element.elements["./ancestor-or-self::[@textLength]/@textLength"]&.value&.to_f
            shield = REXML::Element.new("g")
            width, height = text_length + SHIELD_X * font_size, (1 + SHIELD_Y) * font_size
            shield.add_element "rect", "x" => -0.5 * width, "y" => -0.5 * height, "width" => width, "height" => height, "rx" => font_size * 0.3, "ry" => font_size * 0.3, "stroke" => "none", "fill" => args
            text_transform = element.attributes.get_attribute "transform"
            text_transform.remove
            shield.attributes << text_transform
            element.parent.elements << shield
            shield << element
          when href = element.elements["./textPath[@href]/@href"]&.value
            shield = REXML::Element.new("g")
            shield.add_element "use", "href" => href, "stroke-width" => (1 + SHIELD_Y) * font_size, "stroke" => args, "stroke-linecap" => "round"
            element.parent.elements << shield
            shield << element
          end
        end
      end
    end
  end
end

def to_s

def to_s
  count = features.count
  "%s: %i feature%s" % [@name, count, (?s unless count == 1)]
end