nswtopo 2.0.0.pre.beta1 → 3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +41 -22
- 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
|