nswtopo 2.0.0 → 3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/COPYING +70 -83
- data/bin/nswtopo +227 -116
- data/docs/README.md +1 -12
- data/docs/add.md +1 -1
- data/docs/config.md +1 -1
- data/docs/contours.md +3 -1
- data/docs/init.md +8 -0
- data/docs/inspect.md +103 -0
- data/docs/move.md +9 -0
- data/docs/render.md +16 -7
- data/docs/scrape.md +67 -0
- data/docs/spot-heights.md +6 -2
- data/lib/nswtopo/archive.rb +50 -41
- data/lib/nswtopo/chrome.rb +227 -0
- data/lib/nswtopo/commands/add.rb +106 -0
- data/lib/nswtopo/commands/config.rb +38 -0
- data/lib/nswtopo/commands/inspect.rb +74 -0
- data/lib/nswtopo/commands/layers.rb +22 -0
- data/lib/nswtopo/commands/scrape.rb +79 -0
- data/lib/nswtopo/commands.rb +57 -0
- data/lib/nswtopo/dither.rb +5 -3
- data/lib/nswtopo/font.rb +46 -21
- data/lib/nswtopo/formats/gemf.rb +42 -0
- data/lib/nswtopo/formats/kmz.rb +26 -24
- data/lib/nswtopo/formats/mbtiles.rb +5 -41
- data/lib/nswtopo/formats/pdf.rb +82 -17
- data/lib/nswtopo/formats/svg.rb +114 -45
- data/lib/nswtopo/formats/svgz.rb +2 -2
- data/lib/nswtopo/formats/zip.rb +33 -23
- data/lib/nswtopo/formats.rb +77 -32
- data/lib/nswtopo/geometry/overlap.rb +1 -32
- data/lib/nswtopo/geometry/r_tree.rb +16 -10
- data/lib/nswtopo/geometry/segment.rb +3 -3
- data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +5 -6
- data/lib/nswtopo/geometry/vector_sequence.rb +7 -6
- data/lib/nswtopo/gis/arcgis/connection.rb +56 -0
- data/lib/nswtopo/gis/arcgis/layer/map.rb +163 -0
- data/lib/nswtopo/gis/arcgis/layer/query.rb +87 -0
- data/lib/nswtopo/gis/arcgis/layer/renderer.rb +66 -0
- data/lib/nswtopo/gis/arcgis/layer/statistics.rb +15 -0
- data/lib/nswtopo/gis/arcgis/layer.rb +201 -0
- data/lib/nswtopo/gis/arcgis/service.rb +57 -0
- data/lib/nswtopo/gis/arcgis.rb +3 -0
- data/lib/nswtopo/gis/dem.rb +13 -12
- data/lib/nswtopo/gis/esri_hdr.rb +8 -2
- data/lib/nswtopo/gis/geojson/collection.rb +45 -21
- data/lib/nswtopo/gis/geojson/multi_line_string.rb +2 -24
- data/lib/nswtopo/gis/geojson/multi_polygon.rb +2 -53
- data/lib/nswtopo/gis/geojson/polygon.rb +15 -0
- data/lib/nswtopo/gis/geojson.rb +12 -3
- data/lib/nswtopo/gis/gps/kml.rb +25 -19
- data/lib/nswtopo/gis/gps.rb +2 -0
- data/lib/nswtopo/gis/projection.rb +35 -24
- data/lib/nswtopo/gis/shapefile.rb +89 -16
- data/lib/nswtopo/gis.rb +1 -2
- data/lib/nswtopo/helpers/array.rb +0 -11
- data/lib/nswtopo/helpers/colour.rb +34 -14
- data/lib/nswtopo/layer/arcgis_raster.rb +44 -48
- data/lib/nswtopo/layer/colour_mask.rb +5 -0
- data/lib/nswtopo/layer/contour.rb +35 -28
- data/lib/nswtopo/layer/control.rb +2 -7
- data/lib/nswtopo/layer/declination.rb +9 -9
- data/lib/nswtopo/layer/feature.rb +36 -22
- data/lib/nswtopo/layer/grid.rb +30 -27
- data/lib/nswtopo/layer/import.rb +1 -21
- data/lib/nswtopo/layer/labels/barrier.rb +39 -0
- data/lib/nswtopo/layer/labels.rb +551 -383
- data/lib/nswtopo/layer/mask_render.rb +37 -0
- data/lib/nswtopo/layer/overlay.rb +2 -2
- data/lib/nswtopo/layer/raster.rb +31 -41
- data/lib/nswtopo/layer/raster_import.rb +17 -0
- data/lib/nswtopo/layer/raster_render.rb +15 -0
- data/lib/nswtopo/layer/relief.rb +27 -95
- data/lib/nswtopo/layer/spot.rb +63 -62
- data/lib/nswtopo/layer/vector/cutout.rb +15 -0
- data/lib/nswtopo/layer/vector/knockout.rb +16 -0
- data/lib/nswtopo/layer/vector.rb +121 -89
- data/lib/nswtopo/layer/vegetation.rb +39 -34
- data/lib/nswtopo/layer.rb +30 -16
- data/lib/nswtopo/map.rb +202 -109
- data/lib/nswtopo/os.rb +5 -27
- data/lib/nswtopo/tiled_web_map.rb +54 -0
- data/lib/nswtopo/tree_indenter.rb +27 -0
- data/lib/nswtopo/version.rb +27 -2
- data/lib/nswtopo.rb +6 -199
- metadata +39 -20
- data/lib/nswtopo/font/chrome.rb +0 -59
- data/lib/nswtopo/font/generic.rb +0 -25
- data/lib/nswtopo/gis/arcgis_server/connection.rb +0 -52
- data/lib/nswtopo/gis/arcgis_server.rb +0 -155
- data/lib/nswtopo/gis/geojson/multi_point.rb +0 -12
- data/lib/nswtopo/gis/world_file.rb +0 -19
- data/lib/nswtopo/layer/labels/fence.rb +0 -20
@@ -3,12 +3,12 @@ class RTree
|
|
3
3
|
@nodes, @bounds, @object = nodes, bounds, object
|
4
4
|
end
|
5
5
|
|
6
|
-
def overlaps?(bounds)
|
6
|
+
def overlaps?(bounds, buffer)
|
7
7
|
return false if @bounds.empty?
|
8
8
|
return true unless bounds
|
9
9
|
bounds.zip(@bounds).all? do |bound1, bound2|
|
10
10
|
bound1.zip(bound2.rotate).each.with_index.all? do |limits, index|
|
11
|
-
limits.rotate(index).inject(
|
11
|
+
limits.rotate(index).inject(&:-) <= buffer
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
@@ -31,17 +31,23 @@ class RTree
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
def search(bounds, searched
|
34
|
+
def search(bounds, buffer: 0, searched: Set.new)
|
35
35
|
Enumerator.new do |yielder|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
end
|
41
|
-
yielder << @object if @nodes.empty?
|
36
|
+
next if searched.include? self
|
37
|
+
if overlaps? bounds, buffer
|
38
|
+
@nodes.each do |node|
|
39
|
+
node.search(bounds, buffer: buffer, searched: searched).inject(yielder, &:<<)
|
42
40
|
end
|
43
|
-
|
41
|
+
yielder << @object if @nodes.empty?
|
44
42
|
end
|
43
|
+
searched << self
|
45
44
|
end
|
46
45
|
end
|
46
|
+
|
47
|
+
def each(&block)
|
48
|
+
@nodes.each do |node|
|
49
|
+
node.each(&block)
|
50
|
+
end
|
51
|
+
yield @bounds, @object if @object
|
52
|
+
end
|
47
53
|
end
|
@@ -1,18 +1,18 @@
|
|
1
1
|
module Segment
|
2
2
|
def segments
|
3
|
-
|
3
|
+
each_cons(2).entries
|
4
4
|
end
|
5
5
|
|
6
6
|
def ring
|
7
7
|
zip rotate
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
10
|
+
def diff
|
11
11
|
last.minus first
|
12
12
|
end
|
13
13
|
|
14
14
|
def distance
|
15
|
-
|
15
|
+
diff.norm
|
16
16
|
end
|
17
17
|
|
18
18
|
def along(fraction)
|
@@ -11,13 +11,13 @@ module StraightSkeleton
|
|
11
11
|
@active, @indices = Set[], Hash.new.compare_by_identity
|
12
12
|
data.to_d.map do |points|
|
13
13
|
next points unless points.length > 2
|
14
|
-
points.
|
15
|
-
points
|
14
|
+
points.each.with_object [] do |point, points|
|
15
|
+
points << point unless points.last == point
|
16
16
|
end
|
17
17
|
end.map.with_index do |(*points, point), index|
|
18
18
|
points.first == point ? [points, :ring, (index unless points.hole?)] : [points << point, :segments, nil]
|
19
19
|
end.each do |points, pair, index|
|
20
|
-
normals = points.send(pair).map(&:
|
20
|
+
normals = points.send(pair).map(&:diff).map(&:normalised).map(&:perp)
|
21
21
|
points.map do |point|
|
22
22
|
Vertex.new self, point
|
23
23
|
end.each do |node|
|
@@ -137,7 +137,7 @@ module StraightSkeleton
|
|
137
137
|
end
|
138
138
|
pending.subtract nodes
|
139
139
|
nodes << nodes.first if nodes.first == nodes.last.next
|
140
|
-
result << nodes
|
140
|
+
result << nodes unless nodes.one?
|
141
141
|
end
|
142
142
|
end
|
143
143
|
end
|
@@ -184,10 +184,9 @@ module StraightSkeleton
|
|
184
184
|
end
|
185
185
|
end.each do |point, nodes|
|
186
186
|
@active.subtract nodes
|
187
|
-
nodes.
|
187
|
+
nodes.each.with_object [] do |node, events|
|
188
188
|
events << [:incoming, node.prev] if node.prev
|
189
189
|
events << [:outgoing, node.next] if node.next
|
190
|
-
events
|
191
190
|
end.sort_by do |event, node|
|
192
191
|
case event
|
193
192
|
when :incoming then [-@direction * node.normals[1].angle, 1]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module VectorSequence
|
2
2
|
def perps
|
3
|
-
ring.map(&:
|
3
|
+
ring.map(&:diff).map(&:perp)
|
4
4
|
end
|
5
5
|
|
6
6
|
def signed_area
|
@@ -23,7 +23,7 @@ module VectorSequence
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def convex?
|
26
|
-
ring.map(&:
|
26
|
+
ring.map(&:diff).ring.all? do |directions|
|
27
27
|
directions.inject(&:cross) >= 0
|
28
28
|
end
|
29
29
|
end
|
@@ -101,7 +101,7 @@ module VectorSequence
|
|
101
101
|
end
|
102
102
|
|
103
103
|
def path_length
|
104
|
-
segments.map(&:
|
104
|
+
segments.map(&:diff).sum(&:norm)
|
105
105
|
end
|
106
106
|
|
107
107
|
def trim(margin)
|
@@ -130,9 +130,10 @@ module VectorSequence
|
|
130
130
|
trim(0.5 * (path_length - length))
|
131
131
|
end
|
132
132
|
|
133
|
-
def sample_at(interval, along: false, angle: false)
|
133
|
+
def sample_at(interval, along: false, angle: false, offset: nil)
|
134
134
|
Enumerator.new do |yielder|
|
135
|
-
|
135
|
+
alpha = (0.5 + Float(offset || 0) / interval) % 1.0
|
136
|
+
segments.inject [alpha, 0] do |(alpha, sum), segment|
|
136
137
|
loop do
|
137
138
|
fraction = alpha * interval / segment.distance
|
138
139
|
break unless fraction < 1
|
@@ -140,7 +141,7 @@ module VectorSequence
|
|
140
141
|
sum += alpha * interval
|
141
142
|
yielder << case
|
142
143
|
when along then [segment[0], sum]
|
143
|
-
when angle then [segment[0], segment.
|
144
|
+
when angle then [segment[0], segment.diff.angle]
|
144
145
|
else segment[0]
|
145
146
|
end
|
146
147
|
alpha = 1.0
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module ArcGIS
|
3
|
+
class Connection
|
4
|
+
ERRORS = [Timeout::Error, Errno::ENETUNREACH, Errno::ETIMEDOUT, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, SocketError]
|
5
|
+
Error = Class.new RuntimeError
|
6
|
+
|
7
|
+
def initialize(uri, prefix_path = nil)
|
8
|
+
@http = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 600)
|
9
|
+
@prefix, @headers = Pathname(prefix_path.to_s), { "User-Agent" => "Ruby/#{RUBY_VERSION}", "Referer" => "%s://%s" % [@http.use_ssl? ? "https" : "http", @http.address] }
|
10
|
+
@http.max_retries = 0
|
11
|
+
rescue *ERRORS => error
|
12
|
+
raise Error, error.message
|
13
|
+
end
|
14
|
+
|
15
|
+
def repeatedly_request(request, &block)
|
16
|
+
intervals ||= 4.times.map(&1.4142.method(:**))
|
17
|
+
@http.request(request).tap(&:value).then(&block)
|
18
|
+
rescue *ERRORS, Error => error
|
19
|
+
intervals.any? ? sleep(intervals.shift) : raise(Error, error.message)
|
20
|
+
retry
|
21
|
+
end
|
22
|
+
|
23
|
+
def get(relative_path, **query, &block)
|
24
|
+
path = @prefix.join(relative_path.to_s).to_s
|
25
|
+
path << ?? << URI.encode_www_form(query) unless query.empty?
|
26
|
+
request = Net::HTTP::Get.new(path, @headers)
|
27
|
+
repeatedly_request(request, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def post(relative_path, **query, &block)
|
31
|
+
path = @prefix.join(relative_path.to_s).to_s
|
32
|
+
request = Net::HTTP::Post.new(path, @headers)
|
33
|
+
request.body = URI.encode_www_form(query)
|
34
|
+
repeatedly_request(request, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def process_json(response)
|
38
|
+
JSON.parse(response.body).tap do |result|
|
39
|
+
next unless error = result["error"]
|
40
|
+
# raise Error, error.values_at("message", "details").compact.join(?\n)
|
41
|
+
raise Error, error.values_at("message", "code").map(&:to_s).reject(&:empty?).first
|
42
|
+
end
|
43
|
+
rescue JSON::ParserError
|
44
|
+
raise Error, "unexpected ArcGIS response format"
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_json(relative_path = "", **query)
|
48
|
+
get relative_path, **query, f: "json", &method(:process_json)
|
49
|
+
end
|
50
|
+
|
51
|
+
def post_json(relative_path = "", **query)
|
52
|
+
post relative_path, **query, f: "json", &method(:process_json)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module ArcGIS
|
3
|
+
module Map
|
4
|
+
TILE = 1000
|
5
|
+
NoUniqueFieldError = Class.new RuntimeError
|
6
|
+
|
7
|
+
def pages(per_page)
|
8
|
+
objectid_field = @layer["fields"].find do |field|
|
9
|
+
field["type"] == "esriFieldTypeOID"
|
10
|
+
end&.fetch("name")
|
11
|
+
|
12
|
+
raise "ArcGIS layer does not support dynamic layers: #{@name}" unless @service["supportsDynamicLayers"]
|
13
|
+
raise "ArcGIS layer does not support SVG output: #{@name}" unless @service["supportedImageFormatTypes"].split(?,).include? "SVG"
|
14
|
+
raise "ArcGIS layer does not have an objectid field: #{@name}" unless objectid_field
|
15
|
+
|
16
|
+
@unique ||= @type_field
|
17
|
+
@unique ||= @layer["fields"].find do |field|
|
18
|
+
field.values_at("name", "alias").map(&:downcase).include? @layer.dig("drawingInfo", "renderer", "field1")&.downcase
|
19
|
+
end&.fetch("name")
|
20
|
+
@unique ||= @coded_values.min_by do |name, lookup|
|
21
|
+
lookup.length
|
22
|
+
end&.first
|
23
|
+
raise NoUniqueFieldError unless @unique
|
24
|
+
|
25
|
+
@count = classify(@unique).sum(&:last)
|
26
|
+
return [GeoJSON::Collection.new(projection: projection, name: @name)].each if @count.zero?
|
27
|
+
|
28
|
+
@fields ||= @layer["fields"].select do |field|
|
29
|
+
Layer::FIELD_TYPES === field["type"]
|
30
|
+
end.map do |field|
|
31
|
+
field["name"]
|
32
|
+
end
|
33
|
+
|
34
|
+
include_objectid = @fields.include? objectid_field
|
35
|
+
min, chunk, table = 0, 10000, {}
|
36
|
+
loop do
|
37
|
+
break unless table.length < @count
|
38
|
+
page, where = {}, ["#{objectid_field}>=#{min}", "#{objectid_field}<#{min + chunk}", *@where]
|
39
|
+
Set[*@fields, *extra_field].delete(objectid_field).each_slice(2) do |fields|
|
40
|
+
classify(objectid_field, *fields, where: where).each do |attributes, count|
|
41
|
+
objectid = attributes.delete objectid_field
|
42
|
+
page[objectid] ||= include_objectid ? { objectid_field => objectid } : {}
|
43
|
+
page[objectid].merge! attributes
|
44
|
+
end
|
45
|
+
end
|
46
|
+
rescue Connection::Error
|
47
|
+
(chunk /= 2) > 0 ? retry : raise
|
48
|
+
else
|
49
|
+
table.merge! page
|
50
|
+
min += chunk
|
51
|
+
end
|
52
|
+
|
53
|
+
parent = @layer
|
54
|
+
scale = loop do
|
55
|
+
break parent["minScale"] if parent["minScale"]&.nonzero?
|
56
|
+
break parent["effectiveMinScale"] if parent["effectiveMinScale"]&.nonzero?
|
57
|
+
break unless parent_id = parent.dig("parentLayer", "id")
|
58
|
+
parent = get_json parent_id
|
59
|
+
end || begin
|
60
|
+
case @service["units"]
|
61
|
+
when "esriMeters" then 100000
|
62
|
+
else raise "can't get features from layer: #{@name}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
bounds = @layer["extent"].values_at("xmin", "xmax", "ymin", "ymax").each_slice(2)
|
67
|
+
cx, cy = bounds.map { |bound| 0.5 * bound.sum }
|
68
|
+
bbox, size = %W[#{cx},#{cy},#{cx},#{cy} #{TILE},#{TILE}]
|
69
|
+
dpi = bounds.map { |b0, b1| 0.0254 * TILE * scale / (b1 - b0) }.min * 0.999
|
70
|
+
|
71
|
+
renderer = case @geometry_type
|
72
|
+
when "esriGeometryPoint"
|
73
|
+
{ type: "simple", symbol: { color: [0,0,0,255], size: 1, type: "esriSMS", style: "esriSMSSquare" } }
|
74
|
+
when "esriGeometryPolyline"
|
75
|
+
{ type: "simple", symbol: { color: [0,0,0,255], width: 1, type: "esriSLS", style: "esriSLSSolid" } }
|
76
|
+
when "esriGeometryPolygon"
|
77
|
+
{ type: "simple", symbol: { color: [0,0,0,255], width: 0, type: "esriSFS", style: "esriSFSSolid" } }
|
78
|
+
else
|
79
|
+
raise "unsupported ArcGIS geometry type: #{@geometry_type}"
|
80
|
+
end
|
81
|
+
dynamic_layer = { source: { type: "mapLayer", mapLayerId: @id }, drawingInfo: { showLabels: false, renderer: renderer } }
|
82
|
+
|
83
|
+
sets = table.group_by(&:last).map(&:last).sort_by(&:length)
|
84
|
+
|
85
|
+
Enumerator::Lazy.new(sets) do |yielder, objectids_properties|
|
86
|
+
while objectids_properties.any?
|
87
|
+
begin
|
88
|
+
objectids, properties = objectids_properties.take(per_page).transpose
|
89
|
+
dynamic_layers = [dynamic_layer.merge(definitionExpression: "#{objectid_field} IN (#{objectids.join ?,})")]
|
90
|
+
export = get_json "export", format: "svg", dynamicLayers: dynamic_layers.to_json, bbox: bbox, size: size, mapScale: scale, dpi: dpi
|
91
|
+
href = URI.parse export["href"]
|
92
|
+
xml = Connection.new(href).get(href.path, &:body)
|
93
|
+
xmin, xmax, ymin, ymax = export["extent"].values_at "xmin", "xmax", "ymin", "ymax"
|
94
|
+
rescue Connection::Error
|
95
|
+
(per_page /= 2) > 0 ? retry : raise
|
96
|
+
end
|
97
|
+
|
98
|
+
REXML::Document.new(xml).elements.collect("svg//g[@transform]//g[@transform][path[@d]]") do |group|
|
99
|
+
a, b, c, d, e, f = group.attributes["transform"].match(/matrix\((.*)\)/)[1].split(?\s).map(&:to_f)
|
100
|
+
coords = []
|
101
|
+
group.elements["path[@d]"].attributes["d"].gsub(/\ *([MmZzLlHhVvCcSsQqTtAa])\ */) do
|
102
|
+
?\s + $1 + ?\s
|
103
|
+
end.strip.split(?\s).slice_before(/[MmZzLlHhVvCcSsQqTtAa]/).each do |command, *numbers|
|
104
|
+
raise "can't handle SVG path data command: #{command}" unless numbers.length.even?
|
105
|
+
coordinates = numbers.each_slice(2).map do |x, y|
|
106
|
+
fx, fy = [(a * Float(x) + c * Float(y) + e) / TILE, (b * Float(x) + d * Float(y) + f) / TILE]
|
107
|
+
[fx * xmax + (1 - fx) * xmin, fy * ymin + (1 - fy) * ymax]
|
108
|
+
end
|
109
|
+
case command
|
110
|
+
when ?Z then next
|
111
|
+
when ?M then coords << coordinates
|
112
|
+
when ?L then coords.last.concat coordinates
|
113
|
+
when ?C
|
114
|
+
coordinates.each_slice(3) do |points|
|
115
|
+
raise "unexpected SVG response (bad path data)" unless points.length == 3
|
116
|
+
curves = [[coords.last.last, *points]]
|
117
|
+
while curve = curves.shift
|
118
|
+
next if curve.first == curve.last
|
119
|
+
if curve.values_at(0,-1).distance < 0.99 * curve.segments.map(&:distance).sum
|
120
|
+
reduced = 3.times.inject [ curve ] do |reduced|
|
121
|
+
reduced << reduced.last.each_cons(2).map do |(x0, y0), (x1, y1)|
|
122
|
+
[0.5 * (x0 + x1), 0.5 * (y0 + y1)]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
curves.unshift reduced.map(&:last).reverse
|
126
|
+
curves.unshift reduced.map(&:first)
|
127
|
+
else
|
128
|
+
coords.last << curve.last
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
else raise "can't handle SVG path data command: #{command}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
coords
|
136
|
+
end.tap do |coords|
|
137
|
+
lengths = [properties.length, coords.length]
|
138
|
+
raise "unexpected SVG response (expected %i features, received %i)" % lengths if lengths.inject(&:<)
|
139
|
+
end.zip(properties).map do |coords, properties|
|
140
|
+
case @geometry_type
|
141
|
+
when "esriGeometryPoint"
|
142
|
+
raise "unexpected SVG response (bad point symbol)" unless coords.map(&:length) == [ 4 ]
|
143
|
+
point = coords[0].transpose.map { |coords| coords.sum / coords.length }
|
144
|
+
next GeoJSON::Point.new point, properties
|
145
|
+
when "esriGeometryPolyline"
|
146
|
+
next GeoJSON::LineString.new coords[0], properties if @mixed && coords.one?
|
147
|
+
next GeoJSON::MultiLineString.new coords, properties
|
148
|
+
when "esriGeometryPolygon"
|
149
|
+
coords.each(&:reverse!) unless coords[0].anticlockwise?
|
150
|
+
polys = coords.slice_before(&:anticlockwise?).entries
|
151
|
+
next GeoJSON::Polygon.new polys.first, properties if @mixed && polys.one?
|
152
|
+
next GeoJSON::MultiPolygon.new polys, properties
|
153
|
+
end
|
154
|
+
end.tap do |features|
|
155
|
+
yielder << GeoJSON::Collection.new(projection: projection, name: @name, features: features)
|
156
|
+
end
|
157
|
+
objectids_properties.shift per_page
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module ArcGIS
|
3
|
+
module Query
|
4
|
+
UniqueFieldError = Class.new RuntimeError
|
5
|
+
|
6
|
+
def base_query(**options)
|
7
|
+
case
|
8
|
+
when @unique
|
9
|
+
raise UniqueFieldError
|
10
|
+
when @geometry
|
11
|
+
raise "polgyon geometry required" unless @geometry.polygon?
|
12
|
+
options[:geometry] = { rings: @geometry.reproject_to(projection).coordinates.map(&:reverse) }.to_json
|
13
|
+
options[:geometryType] = "esriGeometryPolygon"
|
14
|
+
options[:where] = join_clauses(*@where) if @where
|
15
|
+
when @where
|
16
|
+
options[:where] = join_clauses(*@where)
|
17
|
+
else
|
18
|
+
oid_field = @layer["fields"].find do |field|
|
19
|
+
field["type"] == "esriFieldTypeOID"
|
20
|
+
end&.fetch("name")
|
21
|
+
options[:where] = oid_field ? "#{oid_field} IS NOT NULL" : "1=1"
|
22
|
+
end
|
23
|
+
options
|
24
|
+
end
|
25
|
+
|
26
|
+
def count
|
27
|
+
@count ||= get_json("#{@id}/query", **base_query, returnCountOnly: true).dig("count")
|
28
|
+
end
|
29
|
+
|
30
|
+
def pages(per_page)
|
31
|
+
objectids = get_json("#{@id}/query", **base_query, returnIdsOnly: true)["objectIds"] || []
|
32
|
+
@count = objectids.count
|
33
|
+
return [GeoJSON::Collection.new(projection: projection, name: @name)].each if @count.zero?
|
34
|
+
|
35
|
+
@fields ||= @layer["fields"].select do |field|
|
36
|
+
Layer::FIELD_TYPES === field["type"]
|
37
|
+
end.map do |field|
|
38
|
+
field["name"]
|
39
|
+
end
|
40
|
+
|
41
|
+
Enumerator.new do |yielder|
|
42
|
+
out_fields = [*@fields, *extra_field].join ?,
|
43
|
+
while objectids.any?
|
44
|
+
begin
|
45
|
+
get_json "#{@id}/query", outFields: out_fields, objectIds: objectids.take(per_page).join(?,)
|
46
|
+
rescue Connection::Error
|
47
|
+
(per_page /= 2) > 0 ? retry : raise
|
48
|
+
end.fetch("features", []).map do |feature|
|
49
|
+
next unless geometry = feature["geometry"]
|
50
|
+
properties = feature.fetch("attributes", {})
|
51
|
+
|
52
|
+
case @geometry_type
|
53
|
+
when "esriGeometryPoint"
|
54
|
+
point = geometry.values_at "x", "y"
|
55
|
+
next unless point.all?
|
56
|
+
next GeoJSON::Point.new point, properties
|
57
|
+
when "esriGeometryMultipoint"
|
58
|
+
points = geometry["points"]
|
59
|
+
next unless points&.any?
|
60
|
+
next GeoJSON::MultiPoint.new points.transpose.take(2).transpose, properties
|
61
|
+
when "esriGeometryPolyline"
|
62
|
+
raise "ArcGIS curve geometries not supported" if geometry.key? "curvePaths"
|
63
|
+
paths = geometry["paths"]
|
64
|
+
next unless paths&.any?
|
65
|
+
next GeoJSON::LineString.new paths[0], properties if @mixed && paths.one?
|
66
|
+
next GeoJSON::MultiLineString.new paths, properties
|
67
|
+
when "esriGeometryPolygon"
|
68
|
+
raise "ArcGIS curve geometries not supported" if geometry.key? "curveRings"
|
69
|
+
rings = geometry["rings"]
|
70
|
+
next unless rings&.any?
|
71
|
+
rings.each(&:reverse!) unless rings[0].anticlockwise?
|
72
|
+
polys = rings.slice_before(&:anticlockwise?).entries
|
73
|
+
next GeoJSON::Polygon.new polys.first, properties if @mixed && polys.one?
|
74
|
+
next GeoJSON::MultiPolygon.new polys, properties
|
75
|
+
else
|
76
|
+
raise "unsupported ArcGIS geometry type: #{@geometry_type}"
|
77
|
+
end
|
78
|
+
end.compact.tap do |features|
|
79
|
+
yielder << GeoJSON::Collection.new(projection: projection, features: features, name: @name)
|
80
|
+
end
|
81
|
+
objectids.shift per_page
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module ArcGIS
|
3
|
+
module Renderer
|
4
|
+
TooManyFieldsError = Class.new RuntimeError
|
5
|
+
NoGeometryError = Class.new RuntimeError
|
6
|
+
|
7
|
+
def classify(*fields, where: @where)
|
8
|
+
raise TooManyFieldsError unless fields.size <= 3
|
9
|
+
raise NoGeometryError, "ArcGIS layer does not support spatial filtering: #{@name}" if @geometry
|
10
|
+
|
11
|
+
types = fields.map do |name|
|
12
|
+
@layer["fields"].find do |field|
|
13
|
+
field["name"] == name
|
14
|
+
end&.fetch("type")
|
15
|
+
end
|
16
|
+
|
17
|
+
counts, values = 2.times do |repeat|
|
18
|
+
counts, values = %w[| ~ ^ #].find do |delimiter|
|
19
|
+
classification_def = { type: "uniqueValueDef", uniqueValueFields: fields.take(repeat) + fields, fieldDelimiter: delimiter }
|
20
|
+
unique_values = get_json("#{@id}/generateRenderer", where: join_clauses(*where), classificationDef: classification_def.to_json).fetch("uniqueValueInfos")
|
21
|
+
|
22
|
+
values = unique_values.map do |info|
|
23
|
+
info["value"].split(delimiter).map(&:strip)
|
24
|
+
end
|
25
|
+
next unless values.all? do |values|
|
26
|
+
values.length == fields.length + repeat
|
27
|
+
end
|
28
|
+
repeat.times { values.each(&:shift) }
|
29
|
+
counts = unique_values.map do |info|
|
30
|
+
info["count"]
|
31
|
+
end
|
32
|
+
break counts, values
|
33
|
+
end
|
34
|
+
raise "couldn't delimit values" unless values
|
35
|
+
next if 0 == repeat && fields.one? && (counts.all?(1) || counts.all?(0))
|
36
|
+
break counts, values
|
37
|
+
end
|
38
|
+
|
39
|
+
values.map do |values|
|
40
|
+
values.zip(types).map do |value, type|
|
41
|
+
case
|
42
|
+
when value == "<Null>" then nil
|
43
|
+
when value == "" then nil
|
44
|
+
when type == "esriFieldTypeOID" then Integer(value)
|
45
|
+
when type == "esriFieldTypeInteger" then Integer(value)
|
46
|
+
when type == "esriFieldTypeSmallInteger" then Integer(value)
|
47
|
+
when type == "esriFieldTypeDouble" then Float(value)
|
48
|
+
when type == "esriFieldTypeSingle" then Float(value)
|
49
|
+
when type == "esriFieldTypeString" then String(value)
|
50
|
+
when type == "esriFieldTypeGUID" then String(value)
|
51
|
+
when type == "esriFieldTypeDate"
|
52
|
+
begin
|
53
|
+
Time.strptime(value, "%m/%d/%Y %l:%M:%S %p").to_i * 1000
|
54
|
+
rescue ArgumentError
|
55
|
+
end
|
56
|
+
end
|
57
|
+
rescue ArgumentError
|
58
|
+
raise "could not interpret #{value.inspect} as #{type}"
|
59
|
+
end.then do |values|
|
60
|
+
fields.zip values
|
61
|
+
end.to_h
|
62
|
+
end.zip counts
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module ArcGIS
|
3
|
+
module Statistics
|
4
|
+
def classify(*fields)
|
5
|
+
statistics = fields.map.with_index do |name, index|
|
6
|
+
{ statisticType: "count", onStatisticField: name, outStatisticFieldName: "COUNT_#{index}" }
|
7
|
+
end
|
8
|
+
field_counts = get_json "#{@id}/query", **base_query, outStatistics: statistics.to_json, groupByFieldsForStatistics: fields.join(?,)
|
9
|
+
field_counts["features"].map do |feature|
|
10
|
+
[feature["attributes"].slice(*fields), feature["attributes"]["COUNT_0"]]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|