7
8
9
10
11
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
# File 'lib/nswtopo/gis/arcgis/layer/map.rb', line 7
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, *].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
|