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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
require_relative 'layer/query'
|
|
2
|
+
require_relative 'layer/map'
|
|
3
|
+
require_relative 'layer/statistics'
|
|
4
|
+
require_relative 'layer/renderer'
|
|
5
|
+
|
|
6
|
+
module NSWTopo
|
|
7
|
+
module ArcGIS
|
|
8
|
+
class Layer
|
|
9
|
+
FIELD_TYPES = %W[esriFieldTypeOID esriFieldTypeInteger esriFieldTypeSmallInteger esriFieldTypeDouble esriFieldTypeSingle esriFieldTypeString esriFieldTypeGUID esriFieldTypeDate].to_set
|
|
10
|
+
NoLayerError = Class.new RuntimeError
|
|
11
|
+
|
|
12
|
+
def initialize(service, id: nil, layer: nil, where: nil, fields: nil, launder: nil, truncate: nil, decode: nil, mixed: true, geometry: nil, unique: nil)
|
|
13
|
+
raise NoLayerError, "no ArcGIS layer name or url provided" unless layer || id
|
|
14
|
+
@id, @name = service["layers"].find do |info|
|
|
15
|
+
layer ? String(layer) == info["name"] : Integer(id) == info["id"]
|
|
16
|
+
end&.values_at("id", "name")
|
|
17
|
+
raise "ArcGIS layer does not exist: #{layer || id}" unless @id
|
|
18
|
+
|
|
19
|
+
@service, @where, @decode, @mixed, @geometry, @unique = service, where, decode, mixed, geometry, unique
|
|
20
|
+
|
|
21
|
+
@layer = get_json @id
|
|
22
|
+
raise "ArcGIS layer is not a feature layer: #{@name}" unless @layer["type"] == "Feature Layer"
|
|
23
|
+
|
|
24
|
+
@geometry_type = @layer["geometryType"]
|
|
25
|
+
|
|
26
|
+
date_fields = @layer["fields"].select do |field|
|
|
27
|
+
"esriFieldTypeDate" == field["type"]
|
|
28
|
+
end.map do |field|
|
|
29
|
+
field["name"]
|
|
30
|
+
end.to_set
|
|
31
|
+
|
|
32
|
+
@fields = fields&.map do |name|
|
|
33
|
+
@layer["fields"].find(-> { raise "invalid field name: #{name}" }) do |field|
|
|
34
|
+
field.values_at("alias", "name").include? name
|
|
35
|
+
end.fetch("name")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
[[%w[typeIdField], %w[subtypeField subtypeFieldName]], %w[types subtypes], %w[id code]].transpose.map do |name_keys, lookup_key, value_key|
|
|
39
|
+
next @layer.values_at(*name_keys).compact.reject(&:empty?).first, @layer[lookup_key], value_key
|
|
40
|
+
end.find do |name_or_alias, lookup, value_key|
|
|
41
|
+
name_or_alias && lookup&.any?
|
|
42
|
+
end&.tap do |name_or_alias, lookup, value_key|
|
|
43
|
+
@type_field = @layer["fields"].find do |field|
|
|
44
|
+
field.values_at("alias", "name").compact.include? name_or_alias
|
|
45
|
+
end&.fetch("name")
|
|
46
|
+
|
|
47
|
+
@type_values = lookup.map do |type|
|
|
48
|
+
type.values_at value_key, "name"
|
|
49
|
+
end.to_h
|
|
50
|
+
|
|
51
|
+
@subtype_values = lookup.map do |type|
|
|
52
|
+
type.values_at value_key, "domains"
|
|
53
|
+
end.map do |code, domains|
|
|
54
|
+
coded_values = domains.map do |name, domain|
|
|
55
|
+
[name, domain["codedValues"]]
|
|
56
|
+
end.select(&:last).map do |name, pairs|
|
|
57
|
+
values = pairs.map do |pair|
|
|
58
|
+
pair.values_at "code", "name"
|
|
59
|
+
end.to_h
|
|
60
|
+
[name, values]
|
|
61
|
+
end.to_h
|
|
62
|
+
[code, coded_values]
|
|
63
|
+
end.to_h
|
|
64
|
+
|
|
65
|
+
@subtype_fields = @subtype_values.values.flat_map(&:keys).uniq
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
@coded_values = @layer["fields"].map do |field|
|
|
69
|
+
[field["name"], field.dig("domain", "codedValues")]
|
|
70
|
+
end.select(&:last).map do |name, pairs|
|
|
71
|
+
values = pairs.map do |pair|
|
|
72
|
+
pair.values_at "code", "name"
|
|
73
|
+
end.to_h
|
|
74
|
+
[name, values]
|
|
75
|
+
end.to_h
|
|
76
|
+
|
|
77
|
+
@rename = @layer["fields"].map do |field|
|
|
78
|
+
field["name"]
|
|
79
|
+
end.map do |name|
|
|
80
|
+
next name, launder ? name.downcase.gsub(/[^\w]+/, ?_) : name
|
|
81
|
+
end.map do |name, substitute|
|
|
82
|
+
next name, truncate ? substitute.slice(0...truncate) : substitute
|
|
83
|
+
end.sort_by do |name, substitute|
|
|
84
|
+
[@fields&.include?(name) ? 0 : 1, substitute == name ? 0 : 1]
|
|
85
|
+
end.inject(Hash[]) do |lookup, (name, substitute)|
|
|
86
|
+
suffix, index, candidate = "_2", 3, substitute
|
|
87
|
+
while lookup.key? candidate
|
|
88
|
+
suffix, index, candidate = "_#{index}", index + 1, (truncate ? substitute.slice(0, truncate - suffix.length) : substitute) + suffix
|
|
89
|
+
raise "can't individualise field name: #{name}" if truncate && suffix.length >= truncate
|
|
90
|
+
end
|
|
91
|
+
lookup.merge candidate => name
|
|
92
|
+
end.invert.to_proc
|
|
93
|
+
|
|
94
|
+
@revalue = lambda do |name, value, properties|
|
|
95
|
+
case
|
|
96
|
+
when %w[null Null NULL <null> <Null> <NULL>].include?(value)
|
|
97
|
+
nil
|
|
98
|
+
when value.nil?
|
|
99
|
+
nil
|
|
100
|
+
when date_fields === name
|
|
101
|
+
Time.at(value / 1000).utc.iso8601
|
|
102
|
+
when !decode
|
|
103
|
+
value
|
|
104
|
+
when @type_field == name
|
|
105
|
+
@type_values[value]
|
|
106
|
+
when lookup = @subtype_values&.dig(properties[@type_field], name)
|
|
107
|
+
lookup[value]
|
|
108
|
+
when lookup = @coded_values.dig(name)
|
|
109
|
+
lookup[value]
|
|
110
|
+
else value
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
case @layer["capabilities"]
|
|
115
|
+
when /Query/ then extend Query, @layer["supportsStatistics"] ? Statistics : Renderer
|
|
116
|
+
when /Map/ then extend Map, Renderer
|
|
117
|
+
else raise "ArcGIS layer does not include Query or Map capability: #{@name}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
extend Forwardable
|
|
122
|
+
delegate %i[get get_json projection] => :@service
|
|
123
|
+
attr_reader :count
|
|
124
|
+
|
|
125
|
+
def extra_field
|
|
126
|
+
case
|
|
127
|
+
when !@decode || !@type_field || !@fields
|
|
128
|
+
when @fields.include?(@type_field)
|
|
129
|
+
when (@subtype_fields & @fields).any? then @type_field
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def decode(attributes)
|
|
134
|
+
attributes.map do |name, value|
|
|
135
|
+
[name, @revalue[name, value, attributes]]
|
|
136
|
+
end.to_h.slice(*@fields).then do |decoded|
|
|
137
|
+
attributes.replace decoded
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def transform(feature)
|
|
142
|
+
decode(feature.properties).transform_keys!(&@rename)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def paged(per_page: nil)
|
|
146
|
+
per_page = [*per_page, *@layer["maxRecordCount"], 500].min
|
|
147
|
+
Enumerator::Lazy.new pages(per_page) do |yielder, page|
|
|
148
|
+
page.each(&method(:transform))
|
|
149
|
+
yielder << page
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def features(**options, &block)
|
|
154
|
+
paged(**options).inject do |collection, page|
|
|
155
|
+
yield collection.count, self.count if block_given?
|
|
156
|
+
collection.merge! page
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def join_clauses(*clauses)
|
|
161
|
+
"(" << clauses.join(") AND (") << ")" if clauses.any?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def codes
|
|
165
|
+
pairs = lambda do |hash|
|
|
166
|
+
hash.keys.zip(hash.values.map(&:sort).map(&:zip)).to_h
|
|
167
|
+
end
|
|
168
|
+
@coded_values.then(&pairs).tap do |result|
|
|
169
|
+
next unless @type_field
|
|
170
|
+
codes, lookups = @subtype_values.sort.transpose
|
|
171
|
+
result[@type_field] = @type_values.slice(*codes).zip lookups.map(&pairs)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def counts
|
|
176
|
+
classify(*@fields, *extra_field).each do |attributes, count|
|
|
177
|
+
decode attributes
|
|
178
|
+
end.group_by(&:first).map do |attributes, attributes_counts|
|
|
179
|
+
[attributes, attributes_counts.sum(&:last)]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def info
|
|
184
|
+
@layer.slice("name", "id").tap do |info|
|
|
185
|
+
info["geometry"] = case @geometry_type
|
|
186
|
+
when "esriGeometryPoint" then "Point"
|
|
187
|
+
when "esriGeometryMultipoint" then "Multipoint"
|
|
188
|
+
when "esriGeometryPolyline" then "LineString"
|
|
189
|
+
when "esriGeometryPolygon" then "Polygon"
|
|
190
|
+
else @geometry_type.delete_prefix("esriGeometry")
|
|
191
|
+
end
|
|
192
|
+
info["EPSG"] = @service["spatialReference"].values_at("latestWkid", "wkid").compact.first
|
|
193
|
+
info["features"] = count
|
|
194
|
+
info["fields"] = @layer["fields"].map do |field|
|
|
195
|
+
[field["name"], field["type"].delete_prefix("esriFieldType")]
|
|
196
|
+
end.sort_by(&:first).to_h if @layer["fields"]&.any?
|
|
197
|
+
end.compact
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module NSWTopo
|
|
2
|
+
module ArcGIS
|
|
3
|
+
class Service
|
|
4
|
+
SERVICE = /^(?:MapServer|FeatureServer|ImageServer)$/
|
|
5
|
+
InvalidURLError = Class.new RuntimeError
|
|
6
|
+
|
|
7
|
+
def self.check_uri(url)
|
|
8
|
+
uri = URI.parse url
|
|
9
|
+
return unless URI::HTTP === uri
|
|
10
|
+
return unless uri.path
|
|
11
|
+
instance, (id, *) = uri.path.split(?/).slice_after(SERVICE).take(2)
|
|
12
|
+
return unless SERVICE === instance&.last
|
|
13
|
+
return unless !id || id =~ /^\d+$/
|
|
14
|
+
return uri, instance.join(?/), id
|
|
15
|
+
rescue URI::Error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.===(string)
|
|
19
|
+
uri, service_path, id = check_uri string
|
|
20
|
+
uri != nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(url)
|
|
24
|
+
uri, service_path, @id = Service.check_uri url
|
|
25
|
+
raise InvalidURLError, "invalid ArcGIS server URL: %s" % url unless uri
|
|
26
|
+
@connection = Connection.new uri, service_path
|
|
27
|
+
@service = get_json ""
|
|
28
|
+
|
|
29
|
+
@projection = case
|
|
30
|
+
when wkt = @service.dig("spatialReference", "wkt") then Projection.new(wkt)
|
|
31
|
+
when wkid = @service.dig("spatialReference", "latestWkid") then Projection.new("EPSG:#{wkid}")
|
|
32
|
+
when wkid = @service.dig("spatialReference", "wkid") then Projection.new("EPSG:#{wkid == 102100 ? 3857 : wkid}")
|
|
33
|
+
else raise "no spatial reference found: #{uri}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
extend Forwardable
|
|
38
|
+
delegate %i[get get_json] => :@connection
|
|
39
|
+
delegate :[] => :@service
|
|
40
|
+
attr_reader :projection
|
|
41
|
+
|
|
42
|
+
def layer(id: @id, **options)
|
|
43
|
+
Layer.new self, id: id, **options
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def layer_info
|
|
47
|
+
children = @service["layers"].group_by do |layer|
|
|
48
|
+
layer["parentLayerId"] || -1
|
|
49
|
+
end
|
|
50
|
+
tree = lambda do |layer|
|
|
51
|
+
[layer.values_at("id", "name").join(": "), children.fetch(layer["id"], []).map(&tree)]
|
|
52
|
+
end
|
|
53
|
+
children[-1].map(&tree)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/nswtopo/gis/dem.rb
CHANGED
|
@@ -5,7 +5,7 @@ module NSWTopo
|
|
|
5
5
|
def get_dem(temp_dir, dem_path)
|
|
6
6
|
txt_path = temp_dir / "dem.txt"
|
|
7
7
|
vrt_path = temp_dir / "dem.vrt"
|
|
8
|
-
|
|
8
|
+
cutline = @map.cutline(**margin)
|
|
9
9
|
|
|
10
10
|
Dir.chdir(@source ? @source.parent : Pathname.pwd) do
|
|
11
11
|
log_update "%s: examining DEM" % @name
|
|
@@ -18,18 +18,20 @@ module NSWTopo
|
|
|
18
18
|
end.group_by do |path, info|
|
|
19
19
|
Projection.new info.dig("coordinateSystem", "wkt")
|
|
20
20
|
end.map.with_index do |(projection, rasters), index|
|
|
21
|
-
raise "DEM data not in planar projection with metre units" unless projection.
|
|
21
|
+
raise "DEM data not in planar projection with metre units" unless projection.metres?
|
|
22
22
|
|
|
23
23
|
paths, resolutions = rasters.map do |path, info|
|
|
24
24
|
[path, info["geoTransform"].values_at(1, 2).norm]
|
|
25
25
|
end.sort_by(&:last).transpose
|
|
26
26
|
|
|
27
27
|
txt_path.write paths.reverse.join(?\n)
|
|
28
|
-
@
|
|
28
|
+
@mm_per_px ||= @map.to_mm(resolutions.first)
|
|
29
29
|
|
|
30
30
|
indexed_dem_path = temp_dir / "dem.#{index}.tif"
|
|
31
|
-
OS.gdalbuildvrt "-overwrite", "-input_file_list", txt_path, vrt_path
|
|
32
|
-
OS.gdalwarp "-t_srs", @map.projection, "-
|
|
31
|
+
OS.gdalbuildvrt "-overwrite", "-allow_projection_difference", "-a_srs", projection, "-input_file_list", txt_path, vrt_path
|
|
32
|
+
OS.gdalwarp "-t_srs", @map.projection, "-tr", @mm_per_px, @mm_per_px, "-r", "bilinear", "-cutline", "GeoJSON:/vsistdin/", "-crop_to_cutline", vrt_path, indexed_dem_path do |stdin|
|
|
33
|
+
stdin.puts cutline.to_json
|
|
34
|
+
end
|
|
33
35
|
indexed_dem_path
|
|
34
36
|
end.tap do |dem_paths|
|
|
35
37
|
txt_path.write dem_paths.join(?\n)
|
|
@@ -39,26 +41,25 @@ module NSWTopo
|
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
def blur_dem(dem_path, blur_path)
|
|
42
|
-
|
|
43
|
-
half = (3 * sigma / @resolution).ceil
|
|
44
|
+
half = (3 * @smooth / @mm_per_px).ceil
|
|
44
45
|
|
|
45
46
|
coeffs = (-half..half).map do |n|
|
|
46
|
-
n * @
|
|
47
|
+
n * @mm_per_px / @smooth
|
|
47
48
|
end.map do |x|
|
|
48
49
|
Math::exp(-x**2)
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
vrt = OS.gdalbuildvrt "/vsistdout/", dem_path
|
|
52
53
|
xml = REXML::Document.new vrt
|
|
53
|
-
xml.elements.each("//ComplexSource") do |
|
|
54
|
-
kernel_filtered_source =
|
|
55
|
-
|
|
54
|
+
xml.elements.each("//ComplexSource|//SimpleSource") do |source|
|
|
55
|
+
kernel_filtered_source = source.parent.add_element("KernelFilteredSource")
|
|
56
|
+
source.elements.each("SourceFilename|OpenOptions|SourceBand|SourceProperties|SrcRect|DstRect") do |element|
|
|
56
57
|
kernel_filtered_source.add_element element
|
|
57
58
|
end
|
|
58
59
|
kernel = kernel_filtered_source.add_element("Kernel", "normalized" => 1)
|
|
59
60
|
kernel.add_element("Size").text = coeffs.size
|
|
60
61
|
kernel.add_element("Coefs").text = coeffs.join ?\s
|
|
61
|
-
|
|
62
|
+
source.parent.delete source
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
log_update "%s: smoothing DEM raster" % @name
|
data/lib/nswtopo/gis/esri_hdr.rb
CHANGED
|
@@ -40,9 +40,15 @@ module NSWTopo
|
|
|
40
40
|
|
|
41
41
|
@values = case path_or_object
|
|
42
42
|
when Pathname
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
data = []
|
|
44
|
+
path_or_object.sub_ext(".bil").open("rb") do |file|
|
|
45
|
+
while !file.eof?
|
|
46
|
+
data += file.read(32*1024*1024).unpack(@format).map do |value|
|
|
47
|
+
value == @nodata ? nil : value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
45
50
|
end
|
|
51
|
+
data
|
|
46
52
|
when ESRIHdr then args[0]
|
|
47
53
|
end
|
|
48
54
|
end
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
module NSWTopo
|
|
2
2
|
module GeoJSON
|
|
3
|
+
DEFAULT_PROJECTION = Projection.wgs84
|
|
4
|
+
|
|
3
5
|
class Collection
|
|
4
|
-
def initialize(projection
|
|
5
|
-
@projection, @features = projection, features
|
|
6
|
+
def initialize(projection: DEFAULT_PROJECTION, features: [], name: nil)
|
|
7
|
+
@projection, @features, @name = projection, features, name
|
|
6
8
|
end
|
|
7
|
-
attr_reader :projection, :features
|
|
9
|
+
attr_reader :projection, :features, :name
|
|
8
10
|
|
|
9
|
-
def self.load(json, projection
|
|
11
|
+
def self.load(json, projection: nil, name: nil)
|
|
10
12
|
collection = JSON.parse(json)
|
|
11
|
-
|
|
12
|
-
projection ||=
|
|
13
|
-
collection["
|
|
13
|
+
crs_name = collection.dig "crs", "properties", "name"
|
|
14
|
+
projection ||= crs_name ? Projection.new(crs_name) : DEFAULT_PROJECTION
|
|
15
|
+
name ||= collection["name"]
|
|
16
|
+
collection["features"].select do |feature|
|
|
17
|
+
feature["geometry"]
|
|
18
|
+
end.map do |feature|
|
|
14
19
|
geometry, properties = feature.values_at "geometry", "properties"
|
|
15
20
|
type, coordinates = geometry.values_at "type", "coordinates"
|
|
16
21
|
raise Error, "unsupported geometry type: #{type}" unless TYPES === type
|
|
17
22
|
GeoJSON.const_get(type).new coordinates, properties
|
|
18
|
-
end.
|
|
19
|
-
new projection, features
|
|
23
|
+
end.then do |features|
|
|
24
|
+
new projection: projection, features: features, name: name
|
|
20
25
|
end
|
|
21
26
|
rescue JSON::ParserError
|
|
22
27
|
raise Error, "invalid GeoJSON data"
|
|
@@ -37,7 +42,7 @@ module NSWTopo
|
|
|
37
42
|
json = OS.ogr2ogr "-t_srs", projection, "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", "GeoJSON:/vsistdin/" do |stdin|
|
|
38
43
|
stdin.puts to_json
|
|
39
44
|
end
|
|
40
|
-
Collection.load json, projection
|
|
45
|
+
Collection.load json, projection: projection
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
def reproject_to_wgs84
|
|
@@ -49,28 +54,30 @@ module NSWTopo
|
|
|
49
54
|
"type" => "FeatureCollection",
|
|
50
55
|
"crs" => { "type" => "name", "properties" => { "name" => @projection } },
|
|
51
56
|
"features" => map(&:to_h)
|
|
52
|
-
}
|
|
57
|
+
}.tap do |hash|
|
|
58
|
+
hash["name"] = @name if @name
|
|
59
|
+
end
|
|
53
60
|
end
|
|
54
61
|
|
|
55
62
|
extend Forwardable
|
|
56
|
-
delegate %i[coordinates properties] => :first
|
|
57
|
-
delegate %i[reject! select!] => :@features
|
|
63
|
+
delegate %i[coordinates properties wkt area] => :first
|
|
64
|
+
delegate %i[reject! select! length] => :@features
|
|
58
65
|
|
|
59
66
|
def to_json(**extras)
|
|
60
67
|
to_h.merge(extras).to_json
|
|
61
68
|
end
|
|
62
69
|
|
|
63
70
|
def explode
|
|
64
|
-
Collection.new @projection, flat_map(&:explode)
|
|
71
|
+
Collection.new projection: @projection, name: @name, features: flat_map(&:explode)
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
def multi
|
|
68
|
-
Collection.new @projection, map(&:multi)
|
|
75
|
+
Collection.new projection: @projection, name: @name, features: map(&:multi)
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
def merge(other)
|
|
72
79
|
raise Error, "can't merge different projections" unless @projection == other.projection
|
|
73
|
-
Collection.new @projection, @features + other.features
|
|
80
|
+
Collection.new projection: @projection, name: @name, features: @features + other.features
|
|
74
81
|
end
|
|
75
82
|
|
|
76
83
|
def merge!(other)
|
|
@@ -78,17 +85,34 @@ module NSWTopo
|
|
|
78
85
|
tap { @features.concat other.features }
|
|
79
86
|
end
|
|
80
87
|
|
|
81
|
-
def clip
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
end.
|
|
85
|
-
|
|
88
|
+
def clip(polygon)
|
|
89
|
+
OS.ogr2ogr "-f", "GeoJSON", "-lco", "RFC7946=NO", "-clipsrc", polygon.wkt, "/vsistdout/", "GeoJSON:/vsistdin/" do |stdin|
|
|
90
|
+
stdin.puts to_json
|
|
91
|
+
end.then do |json|
|
|
92
|
+
Collection.load json, projection: @projection
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def buffer(*margins, **options)
|
|
97
|
+
map do |feature|
|
|
98
|
+
feature.buffer(*margins, **options)
|
|
99
|
+
end.then do |features|
|
|
100
|
+
Collection.new projection: @projection, name: @name, features: features
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def rename(name = nil)
|
|
105
|
+
tap { @name = name }
|
|
86
106
|
end
|
|
87
107
|
|
|
88
108
|
# TODO: what about empty collections?
|
|
89
109
|
def bounds
|
|
90
110
|
map(&:bounds).transpose.map(&:flatten).map(&:minmax)
|
|
91
111
|
end
|
|
112
|
+
|
|
113
|
+
def bbox
|
|
114
|
+
GeoJSON.polygon [bounds.inject(&:product).values_at(0,2,3,1,0)], projection: @projection
|
|
115
|
+
end
|
|
92
116
|
end
|
|
93
117
|
end
|
|
94
118
|
end
|
|
@@ -3,28 +3,6 @@ module NSWTopo
|
|
|
3
3
|
class MultiLineString
|
|
4
4
|
include StraightSkeleton
|
|
5
5
|
|
|
6
|
-
def clip(hull)
|
|
7
|
-
lines = [hull, hull.perps].transpose.inject(@coordinates) do |result, (vertex, perp)|
|
|
8
|
-
result.inject([]) do |clipped, points|
|
|
9
|
-
clipped + [*points, points.last].segments.inject([[]]) do |lines, segment|
|
|
10
|
-
inside = segment.map { |point| point.minus(vertex).dot(perp) >= 0 }
|
|
11
|
-
case
|
|
12
|
-
when inside.all?
|
|
13
|
-
lines.last << segment[0]
|
|
14
|
-
when inside[0]
|
|
15
|
-
lines.last << segment[0]
|
|
16
|
-
lines.last << segment.along(vertex.minus(segment[0]).dot(perp) / segment.difference.dot(perp))
|
|
17
|
-
when inside[1]
|
|
18
|
-
lines << []
|
|
19
|
-
lines.last << segment.along(vertex.minus(segment[0]).dot(perp) / segment.difference.dot(perp))
|
|
20
|
-
end
|
|
21
|
-
lines
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end.select(&:many?)
|
|
25
|
-
lines.none? ? nil : lines.one? ? LineString.new(*lines, @properties) : MultiLineString.new(lines, @properties)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
6
|
def length
|
|
29
7
|
@coordinates.sum(&:path_length)
|
|
30
8
|
end
|
|
@@ -50,12 +28,12 @@ module NSWTopo
|
|
|
50
28
|
end
|
|
51
29
|
|
|
52
30
|
def samples(interval)
|
|
53
|
-
points = @coordinates.
|
|
31
|
+
points = @coordinates.flat_map do |linestring|
|
|
54
32
|
distance = linestring.path_length
|
|
55
33
|
linestring.sample_at(interval, along: true).map do |point, along|
|
|
56
34
|
[point, (2 * along - distance).abs - distance]
|
|
57
35
|
end
|
|
58
|
-
end.
|
|
36
|
+
end.sort_by(&:last).map(&:first)
|
|
59
37
|
MultiPoint.new points, @properties
|
|
60
38
|
end
|
|
61
39
|
end
|
|
@@ -3,57 +3,6 @@ module NSWTopo
|
|
|
3
3
|
class MultiPolygon
|
|
4
4
|
include StraightSkeleton
|
|
5
5
|
|
|
6
|
-
def clip(hull)
|
|
7
|
-
polys = @coordinates.inject([]) do |result, rings|
|
|
8
|
-
lefthanded = rings.first.clockwise?
|
|
9
|
-
interior, exterior = hull.zip(hull.perps).inject(rings) do |rings, (vertex, perp)|
|
|
10
|
-
insides, neighbours, clipped = Hash[].compare_by_identity, Hash[].compare_by_identity, []
|
|
11
|
-
rings.each do |points|
|
|
12
|
-
points.map do |point|
|
|
13
|
-
point.minus(vertex).dot(perp) >= 0
|
|
14
|
-
end.segments.zip(points.segments).each do |inside, segment|
|
|
15
|
-
insides[segment] = inside
|
|
16
|
-
neighbours[segment] = [nil, nil]
|
|
17
|
-
end.map(&:last).ring.each do |segment0, segment1|
|
|
18
|
-
neighbours[segment1][0], neighbours[segment0][1] = segment0, segment1
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
neighbours.select! do |segment, _|
|
|
22
|
-
insides[segment].any?
|
|
23
|
-
end
|
|
24
|
-
insides.select do |segment, inside|
|
|
25
|
-
inside.inject(&:^)
|
|
26
|
-
end.each do |segment, inside|
|
|
27
|
-
segment[inside[0] ? 1 : 0] = segment.along(vertex.minus(segment[0]).dot(perp) / segment.difference.dot(perp))
|
|
28
|
-
end.sort_by do |segment, inside|
|
|
29
|
-
segment[inside[0] ? 1 : 0].minus(vertex).cross(perp) * (lefthanded ? -1 : 1)
|
|
30
|
-
end.map(&:first).each_slice(2) do |segment0, segment1|
|
|
31
|
-
segment = [segment0[1], segment1[0]]
|
|
32
|
-
neighbours[segment0][1] = neighbours[segment1][0] = segment
|
|
33
|
-
neighbours[segment] = [segment0, segment1]
|
|
34
|
-
end
|
|
35
|
-
while neighbours.any?
|
|
36
|
-
segment, * = neighbours.first
|
|
37
|
-
clipped << []
|
|
38
|
-
while neighbours.include? segment
|
|
39
|
-
clipped.last << segment[0]
|
|
40
|
-
*, segment = neighbours.delete(segment)
|
|
41
|
-
end
|
|
42
|
-
clipped.last << clipped.last.first
|
|
43
|
-
end
|
|
44
|
-
clipped
|
|
45
|
-
end.partition(&:clockwise?).rotate(lefthanded ? 1 : 0)
|
|
46
|
-
next result << exterior + interior if exterior.one?
|
|
47
|
-
exterior.inject(result) do |result, exterior_ring|
|
|
48
|
-
within, interior = interior.partition do |interior_ring|
|
|
49
|
-
interior_ring.first.within? exterior_ring
|
|
50
|
-
end
|
|
51
|
-
result << [exterior_ring, *within]
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
polys.none? ? nil : polys.one? ? Polygon.new(*polys, @properties) : MultiPolygon.new(polys, @properties)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
6
|
def area
|
|
58
7
|
@coordinates.flatten(1).sum(&:signed_area)
|
|
59
8
|
end
|
|
@@ -119,13 +68,13 @@ module NSWTopo
|
|
|
119
68
|
lengths[index], lines[index] = length + tail_length, nodes + tail_nodes.reverse if length + tail_length > lengths[index]
|
|
120
69
|
end
|
|
121
70
|
|
|
122
|
-
linestrings = lines.values.
|
|
71
|
+
linestrings = lines.values.flat_map do |nodes|
|
|
123
72
|
nodes.chunk do |node|
|
|
124
73
|
node.travel >= min_travel
|
|
125
74
|
end.select(&:first).map(&:last).reject(&:one?).map do |nodes|
|
|
126
75
|
nodes.map(&:point).to_f
|
|
127
76
|
end
|
|
128
|
-
end
|
|
77
|
+
end
|
|
129
78
|
features.prepend MultiLineString.new(linestrings, @properties)
|
|
130
79
|
end
|
|
131
80
|
|
|
@@ -3,9 +3,24 @@ module NSWTopo
|
|
|
3
3
|
class Polygon
|
|
4
4
|
delegate %i[area skeleton centres centrepoints centrelines buffer centroids samples] => :multi
|
|
5
5
|
|
|
6
|
+
def validate!
|
|
7
|
+
@coordinates.inject(false) do |hole, ring|
|
|
8
|
+
ring.reverse! if hole ^ ring.hole?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
6
13
|
def bounds
|
|
7
14
|
@coordinates.first.transpose.map(&:minmax)
|
|
8
15
|
end
|
|
16
|
+
|
|
17
|
+
def wkt
|
|
18
|
+
@coordinates.map do |ring|
|
|
19
|
+
ring.map do |point|
|
|
20
|
+
point.join(" ")
|
|
21
|
+
end.join(", ").prepend("(").concat(")")
|
|
22
|
+
end.join(", ").prepend("POLYGON (").concat(")")
|
|
23
|
+
end
|
|
9
24
|
end
|
|
10
25
|
end
|
|
11
26
|
end
|
data/lib/nswtopo/gis/geojson.rb
CHANGED
|
@@ -12,6 +12,7 @@ module NSWTopo
|
|
|
12
12
|
raise Error, "invalid feature properties" unless Hash === properties
|
|
13
13
|
raise Error, "invalid feature geometry" unless Array === coordinates
|
|
14
14
|
@coordinates, @properties = coordinates, properties
|
|
15
|
+
validate!
|
|
15
16
|
end
|
|
16
17
|
attr_accessor :coordinates, :properties
|
|
17
18
|
|
|
@@ -42,8 +43,12 @@ module NSWTopo
|
|
|
42
43
|
grep klass
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
Collection.define_method "#{type}?".downcase do
|
|
47
|
+
one? && klass === first
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
define_singleton_method type.downcase do |coordinates, projection: DEFAULT_PROJECTION, name: nil, properties: {}|
|
|
51
|
+
Collection.new(projection: projection, name: name) << klass.new(coordinates, properties)
|
|
47
52
|
end
|
|
48
53
|
end
|
|
49
54
|
|
|
@@ -60,6 +65,7 @@ module NSWTopo
|
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
delegate :clip => :multi
|
|
68
|
+
alias validate! itself
|
|
63
69
|
end
|
|
64
70
|
|
|
65
71
|
multi_class.class_eval do
|
|
@@ -69,6 +75,10 @@ module NSWTopo
|
|
|
69
75
|
end
|
|
70
76
|
end
|
|
71
77
|
|
|
78
|
+
def validate!
|
|
79
|
+
explode.each &:validate!
|
|
80
|
+
end
|
|
81
|
+
|
|
72
82
|
def bounds
|
|
73
83
|
explode.map(&:bounds).transpose.map(&:flatten).map(&:minmax)
|
|
74
84
|
end
|
|
@@ -84,6 +94,5 @@ end
|
|
|
84
94
|
require_relative 'geojson/point'
|
|
85
95
|
require_relative 'geojson/line_string'
|
|
86
96
|
require_relative 'geojson/polygon'
|
|
87
|
-
require_relative 'geojson/multi_point'
|
|
88
97
|
require_relative 'geojson/multi_line_string'
|
|
89
98
|
require_relative 'geojson/multi_polygon'
|