Class: NSWTopo::ArcGIS::Layer

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/nswtopo/gis/arcgis/layer.rb

Constant Summary collapse

FIELD_TYPES =
%W[esriFieldTypeOID esriFieldTypeInteger esriFieldTypeSmallInteger esriFieldTypeDouble esriFieldTypeSingle esriFieldTypeString esriFieldTypeGUID esriFieldTypeDate].to_set
NoLayerError =
Class.new RuntimeError

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(service, id: nil, layer: nil, where: nil, fields: nil, launder: nil, truncate: nil, decode: nil, mixed: true, geometry: nil, unique: nil) ⇒ Layer

Returns a new instance of Layer.

Raises:



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/nswtopo/gis/arcgis/layer.rb', line 12

def initialize(service, id: nil, layer: nil, where: nil, fields: nil, launder: nil, truncate: nil, decode: nil, mixed: true, geometry: nil, unique: nil)
  raise NoLayerError, "no ArcGIS layer name or url provided" unless layer || id
  @id, @name = service["layers"].find do |info|
    layer ? String(layer) == info["name"] : Integer(id) == info["id"]
  end&.values_at("id", "name")
  raise "ArcGIS layer does not exist: #{layer || id}" unless @id

  @service, @where, @decode, @mixed, @geometry, @unique = service, where, decode, mixed, geometry, unique

  @layer = get_json @id
  raise "ArcGIS layer is not a feature layer: #{@name}" unless @layer["type"] == "Feature Layer"

  @geometry_type = @layer["geometryType"]

  date_fields = @layer["fields"].select do |field|
    "esriFieldTypeDate" == field["type"]
  end.map do |field|
    field["name"]
  end.to_set

  @fields = fields&.map do |name|
    @layer["fields"].find(-> { raise "invalid field name: #{name}" }) do |field|
      field.values_at("alias", "name").include? name
    end.fetch("name")
  end

  [[%w[typeIdField], %w[subtypeField subtypeFieldName]], %w[types subtypes], %w[id code]].transpose.map do |name_keys, lookup_key, value_key|
    next @layer.values_at(*name_keys).compact.reject(&:empty?).first, @layer[lookup_key], value_key
  end.find do |name_or_alias, lookup, value_key|
    name_or_alias && lookup&.any?
  end&.tap do |name_or_alias, lookup, value_key|
    @type_field = @layer["fields"].find do |field|
      field.values_at("alias", "name").compact.include? name_or_alias
    end&.fetch("name")

    @type_values = lookup.map do |type|
      type.values_at value_key, "name"
    end.to_h

    @subtype_values = lookup.map do |type|
      type.values_at value_key, "domains"
    end.map do |code, domains|
      coded_values = domains.map do |name, domain|
        [name, domain["codedValues"]]
      end.select(&:last).map do |name, pairs|
        values = pairs.map do |pair|
          pair.values_at "code", "name"
        end.to_h
        [name, values]
      end.to_h
      [code, coded_values]
    end.to_h

    @subtype_fields = @subtype_values.values.flat_map(&:keys).uniq
  end

  @coded_values = @layer["fields"].map do |field|
    [field["name"], field.dig("domain", "codedValues")]
  end.select(&:last).map do |name, pairs|
    values = pairs.map do |pair|
      pair.values_at "code", "name"
    end.to_h
    [name, values]
  end.to_h

  @rename = @layer["fields"].map do |field|
    field["name"]
  end.map do |name|
    next name, launder ? name.downcase.gsub(/[^\w]+/, ?_) : name
  end.map do |name, substitute|
    next name, truncate ? substitute.slice(0...truncate) : substitute
  end.sort_by do |name, substitute|
    [@fields&.include?(name) ? 0 : 1, substitute == name ? 0 : 1]
  end.inject(Hash[]) do |lookup, (name, substitute)|
    suffix, index, candidate = "_2", 3, substitute
    while lookup.key? candidate
      suffix, index, candidate = "_#{index}", index + 1, (truncate ? substitute.slice(0, truncate - suffix.length) : substitute) + suffix
      raise "can't individualise field name: #{name}" if truncate && suffix.length >= truncate
    end
    lookup.merge candidate => name
  end.invert.to_proc

  @revalue = lambda do |name, value, properties|
    case
    when %w[null Null NULL <null> <Null> <NULL>].include?(value)
      nil
    when value.nil?
      nil
    when date_fields === name
      Time.at(value / 1000).utc.iso8601
    when !decode
      value
    when @type_field == name
      @type_values[value]
    when lookup = @subtype_values&.dig(properties[@type_field], name)
      lookup[value]
    when lookup = @coded_values.dig(name)
      lookup[value]
    else value
    end
  end

  case @layer["capabilities"]
  when /Query/ then extend Query, @layer["supportsStatistics"] ? Statistics : Renderer
  when /Map/   then extend Map, Renderer
  else raise "ArcGIS layer does not include Query or Map capability: #{@name}"
  end
end

Instance Attribute Details

#countObject (readonly)

Returns the value of attribute count.



123
124
125
# File 'lib/nswtopo/gis/arcgis/layer.rb', line 123

def count
  @count
end

Instance Method Details

#codesObject



160
161
162
163
164
165
166
167
168
169
# File 'lib/nswtopo/gis/arcgis/layer.rb', line 160

def codes
  pairs = lambda do |hash|
    hash.keys.zip(hash.values.map(&:sort).map(&:zip)).to_h
  end
  @coded_values.then(&pairs).tap do |result|
    next unless @type_field
    codes, lookups = @subtype_values.sort.transpose
    result[@type_field] = @type_values.slice(*codes).zip lookups.map(&pairs)
  end
end

#countsObject



171
172
173
174
175
176
177
# File 'lib/nswtopo/gis/arcgis/layer.rb', line 171

def counts
  classify(*@fields, *extra_field).group_by do |attributes, count|
    decode attributes
  end.map do |attributes, attributes_counts|
    [attributes, attributes_counts.sum(&:last)]
  end
end

#decode(attributes) ⇒ Object



133
134
135
136
137
# File 'lib/nswtopo/gis/arcgis/layer.rb', line 133

def decode(attributes)
  attributes.map do |name, value|
    [name, @revalue[name, value, attributes]]
  end.to_h.slice(*@fields)
end

#extra_fieldObject



125
126
127
128
129
130
131
# File 'lib/nswtopo/gis/arcgis/layer.rb', line 125

def extra_field
  case
  when !@decode || !@type_field || !@fields
  when @fields.include?(@type_field)
  when (@subtype_fields & @fields).any? then @type_field
  end
end

#features(**options, &block) ⇒ Object



149
150
151
152
153
154
# File 'lib/nswtopo/gis/arcgis/layer.rb', line 149

def features(**options, &block)
  paged(**options).inject do |collection, page|
    yield collection.count, self.count if block_given?
    collection.merge! page
  end
end

#infoObject



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/nswtopo/gis/arcgis/layer.rb', line 179

def info
  @layer.slice("name", "id").tap do |info|
    info["geometry"] = case @geometry_type
    when "esriGeometryPoint" then "Point"
    when "esriGeometryMultipoint" then "Multipoint"
    when "esriGeometryPolyline" then "LineString"
    when "esriGeometryPolygon" then "Polygon"
    else @geometry_type.delete_prefix("esriGeometry")
    end
    info["EPSG"] = @service["spatialReference"].values_at("latestWkid", "wkid").compact.first
    info["features"] = count
    info["fields"] = @layer["fields"].map do |field|
      [field["name"], field["type"].delete_prefix("esriFieldType")]
    end.sort_by(&:first).to_h if @layer["fields"]&.any?
  end.compact
end

#join_clauses(*clauses) ⇒ Object



156
157
158
# File 'lib/nswtopo/gis/arcgis/layer.rb', line 156

def join_clauses(*clauses)
  "(" << clauses.join(") AND (") << ")" if clauses.any?
end

#paged(per_page: nil) ⇒ Object



139
140
141
142
143
144
145
146
147
# File 'lib/nswtopo/gis/arcgis/layer.rb', line 139

def paged(per_page: nil)
  per_page = [*per_page, *@layer["maxRecordCount"], 500].min
  Enumerator::Lazy.new pages(per_page) do |yielder, page|
    page.map! do |feature|
      decoded = decode(feature.properties).transform_keys!(&@rename)
      feature.with_properties decoded
    end.then(&yielder)
  end
end