nswtopo 2.0.0.pre.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/COPYING +674 -0
- data/bin/nswtopo +430 -0
- data/docs/README.md +78 -0
- data/docs/add.md +49 -0
- data/docs/config.md +24 -0
- data/docs/contours.md +37 -0
- data/docs/controls.md +9 -0
- data/docs/declination.md +15 -0
- data/docs/delete.md +15 -0
- data/docs/grid.md +5 -0
- data/docs/info.md +5 -0
- data/docs/init.md +38 -0
- data/docs/layers.md +11 -0
- data/docs/overlay.md +37 -0
- data/docs/relief.md +22 -0
- data/docs/render.md +43 -0
- data/docs/spot-heights.md +23 -0
- data/lib/nswtopo/archive.rb +93 -0
- data/lib/nswtopo/avl_tree.rb +128 -0
- data/lib/nswtopo/config.rb +73 -0
- data/lib/nswtopo/dither.rb +31 -0
- data/lib/nswtopo/font/chrome.rb +59 -0
- data/lib/nswtopo/font/generic.rb +25 -0
- data/lib/nswtopo/font.rb +43 -0
- data/lib/nswtopo/formats/kmz.rb +149 -0
- data/lib/nswtopo/formats/mbtiles.rb +64 -0
- data/lib/nswtopo/formats/pdf.rb +31 -0
- data/lib/nswtopo/formats/svg.rb +69 -0
- data/lib/nswtopo/formats/svgz.rb +13 -0
- data/lib/nswtopo/formats/zip.rb +40 -0
- data/lib/nswtopo/formats.rb +76 -0
- data/lib/nswtopo/geometry/overlap.rb +78 -0
- data/lib/nswtopo/geometry/r_tree.rb +47 -0
- data/lib/nswtopo/geometry/segment.rb +27 -0
- data/lib/nswtopo/geometry/straight_skeleton/collapse.rb +21 -0
- data/lib/nswtopo/geometry/straight_skeleton/interior_node.rb +17 -0
- data/lib/nswtopo/geometry/straight_skeleton/node.rb +50 -0
- data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +295 -0
- data/lib/nswtopo/geometry/straight_skeleton/split.rb +33 -0
- data/lib/nswtopo/geometry/straight_skeleton/vertex.rb +9 -0
- data/lib/nswtopo/geometry/straight_skeleton.rb +6 -0
- data/lib/nswtopo/geometry/vector.rb +91 -0
- data/lib/nswtopo/geometry/vector_sequence.rb +179 -0
- data/lib/nswtopo/geometry.rb +8 -0
- data/lib/nswtopo/gis/arcgis_server/connection.rb +52 -0
- data/lib/nswtopo/gis/arcgis_server.rb +155 -0
- data/lib/nswtopo/gis/dem.rb +70 -0
- data/lib/nswtopo/gis/esri_hdr.rb +77 -0
- data/lib/nswtopo/gis/gdal_glob.rb +41 -0
- data/lib/nswtopo/gis/geojson/collection.rb +94 -0
- data/lib/nswtopo/gis/geojson/line_string.rb +11 -0
- data/lib/nswtopo/gis/geojson/multi_line_string.rb +63 -0
- data/lib/nswtopo/gis/geojson/multi_point.rb +12 -0
- data/lib/nswtopo/gis/geojson/multi_polygon.rb +167 -0
- data/lib/nswtopo/gis/geojson/point.rb +9 -0
- data/lib/nswtopo/gis/geojson/polygon.rb +11 -0
- data/lib/nswtopo/gis/geojson.rb +89 -0
- data/lib/nswtopo/gis/gps/gpx.rb +22 -0
- data/lib/nswtopo/gis/gps/kml.rb +66 -0
- data/lib/nswtopo/gis/gps.rb +20 -0
- data/lib/nswtopo/gis/projection.rb +56 -0
- data/lib/nswtopo/gis/shapefile.rb +24 -0
- data/lib/nswtopo/gis/world_file.rb +19 -0
- data/lib/nswtopo/gis.rb +9 -0
- data/lib/nswtopo/help_formatter.rb +59 -0
- data/lib/nswtopo/helpers/array.rb +30 -0
- data/lib/nswtopo/helpers/colour.rb +176 -0
- data/lib/nswtopo/helpers/concurrently.rb +27 -0
- data/lib/nswtopo/helpers/dir.rb +7 -0
- data/lib/nswtopo/helpers/hash.rb +15 -0
- data/lib/nswtopo/helpers/tar_writer.rb +11 -0
- data/lib/nswtopo/helpers.rb +6 -0
- data/lib/nswtopo/layer/arcgis_raster.rb +73 -0
- data/lib/nswtopo/layer/contour.rb +233 -0
- data/lib/nswtopo/layer/control.rb +94 -0
- data/lib/nswtopo/layer/declination.rb +53 -0
- data/lib/nswtopo/layer/feature.rb +87 -0
- data/lib/nswtopo/layer/grid.rb +120 -0
- data/lib/nswtopo/layer/import.rb +25 -0
- data/lib/nswtopo/layer/labels/fence.rb +20 -0
- data/lib/nswtopo/layer/labels.rb +630 -0
- data/lib/nswtopo/layer/overlay.rb +53 -0
- data/lib/nswtopo/layer/raster.rb +63 -0
- data/lib/nswtopo/layer/relief.rb +143 -0
- data/lib/nswtopo/layer/spot.rb +171 -0
- data/lib/nswtopo/layer/vector.rb +263 -0
- data/lib/nswtopo/layer/vegetation.rb +73 -0
- data/lib/nswtopo/layer.rb +78 -0
- data/lib/nswtopo/log.rb +28 -0
- data/lib/nswtopo/map.rb +296 -0
- data/lib/nswtopo/os.rb +75 -0
- data/lib/nswtopo/safely.rb +13 -0
- data/lib/nswtopo/version.rb +4 -0
- data/lib/nswtopo/zip.rb +15 -0
- data/lib/nswtopo.rb +249 -0
- metadata +142 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module ArcGISServer
|
3
|
+
class Connection
|
4
|
+
def initialize(http, service_path)
|
5
|
+
@http, @service_path, @headers = http, service_path, { "User-Agent" => "Ruby/#{RUBY_VERSION}", "Referer" => "%s://%s" % [http.use_ssl? ? "https" : "http", http.address] }
|
6
|
+
http.max_retries = 0
|
7
|
+
end
|
8
|
+
|
9
|
+
def repeatedly_request(request)
|
10
|
+
intervals ||= 5.times.map(&1.4142.method(:**))
|
11
|
+
response = @http.request(request)
|
12
|
+
response.error! unless Net::HTTPSuccess === response
|
13
|
+
yield response
|
14
|
+
rescue *ERRORS, Error => error
|
15
|
+
interval = intervals.shift
|
16
|
+
interval ? sleep(interval) : raise(error)
|
17
|
+
retry
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(relative_path, **query, &block)
|
21
|
+
path = Pathname(@service_path).join(relative_path).to_s
|
22
|
+
path << ?? << URI.encode_www_form(query) unless query.empty?
|
23
|
+
request = Net::HTTP::Get.new(path, @headers)
|
24
|
+
repeatedly_request(request, &block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def post(relative_path, **query, &block)
|
28
|
+
path = Pathname(@service_path).join(relative_path).to_s
|
29
|
+
request = Net::HTTP::Post.new(path, @headers)
|
30
|
+
request.body = URI.encode_www_form(query)
|
31
|
+
repeatedly_request(request, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def process_json(response)
|
35
|
+
JSON.parse(response.body).tap do |result|
|
36
|
+
# raise Error, result["error"].values_at("message", "details").compact.join(?\n) if result["error"]
|
37
|
+
raise Error, result["error"]["message"] if result["error"]
|
38
|
+
end
|
39
|
+
rescue JSON::ParserError
|
40
|
+
raise Error, "unexpected ArcGIS response format"
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_json(relative_path = "", **query)
|
44
|
+
get relative_path, query.merge(f: "json"), &method(:process_json)
|
45
|
+
end
|
46
|
+
|
47
|
+
def post_json(relative_path = "", **query)
|
48
|
+
post relative_path, query.merge(f: "json"), &method(:process_json)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require_relative 'arcgis_server/connection'
|
2
|
+
|
3
|
+
module NSWTopo
|
4
|
+
module ArcGISServer
|
5
|
+
Error = Class.new RuntimeError
|
6
|
+
ERRORS = [Timeout::Error, Errno::ENETUNREACH, Errno::ETIMEDOUT, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, SocketError]
|
7
|
+
SERVICE = /^(?:MapServer|FeatureServer|ImageServer)$/
|
8
|
+
|
9
|
+
def self.check_uri(url)
|
10
|
+
uri = URI.parse url
|
11
|
+
return unless URI::HTTP === uri
|
12
|
+
instance, (id, *) = uri.path.split(?/).slice_after(SERVICE).take(2)
|
13
|
+
return unless instance.last =~ SERVICE
|
14
|
+
return unless !id || id =~ /^\d+$/
|
15
|
+
return uri, instance.join(?/), id
|
16
|
+
rescue URI::Error
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.===(string)
|
20
|
+
uri, service_path, id = check_uri string
|
21
|
+
uri != nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.start(url, &block)
|
25
|
+
uri, service_path, id = check_uri url
|
26
|
+
raise "invalid ArcGIS server URL: %s" % url unless uri
|
27
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: 600) do |http|
|
28
|
+
connection = Connection.new http, service_path
|
29
|
+
service = connection.get_json
|
30
|
+
projection = case
|
31
|
+
when wkt = service.dig("spatialReference", "wkt") then Projection.new(wkt)
|
32
|
+
when wkid = service.dig("spatialReference", "latestWkid") then Projection.new("EPSG:#{wkid}")
|
33
|
+
when wkid = service.dig("spatialReference", "wkid") then Projection.new("EPSG:#{wkid == 102100 ? 3857 : wkid}")
|
34
|
+
else raise Error, "no spatial reference found: #{uri}"
|
35
|
+
end
|
36
|
+
yield connection, service, projection, *id
|
37
|
+
end
|
38
|
+
rescue *ERRORS => error
|
39
|
+
raise Error, error.message
|
40
|
+
end
|
41
|
+
|
42
|
+
def arcgis_layer(url, where: nil, layer: nil, per_page: nil, margin: {})
|
43
|
+
ArcGISServer.start url do |connection, service, projection, id|
|
44
|
+
id = service["layers"].find do |info|
|
45
|
+
layer.to_s == info["name"]
|
46
|
+
end&.dig("id") if layer
|
47
|
+
id ? nil : layer ? raise("no such ArcGIS layer: %s" % layer) : raise("not an ArcGIS layer url: %s" % url)
|
48
|
+
|
49
|
+
layer = connection.get_json id.to_s
|
50
|
+
query_path = "#{id}/query"
|
51
|
+
max_record_count, fields, types, type_id_field, geometry_type, capabilities = layer.values_at "maxRecordCount", "fields", "types", "typeIdField", "geometryType", "capabilities"
|
52
|
+
raise Error, "no query capability available: #{url}" unless capabilities =~ /Query|Data/
|
53
|
+
|
54
|
+
if type_id_field && types
|
55
|
+
type_id_field = fields.find do |field|
|
56
|
+
field.values_at("alias", "name").include? type_id_field
|
57
|
+
end&.fetch("name")
|
58
|
+
type_values = types.map do |type|
|
59
|
+
type.values_at "id", "name"
|
60
|
+
end.to_h
|
61
|
+
subtype_coded_values = types.map do |type|
|
62
|
+
type.values_at "id", "domains"
|
63
|
+
end.map do |id, domains|
|
64
|
+
coded_values = domains.map do |name, domain|
|
65
|
+
[name, domain["codedValues"]]
|
66
|
+
end.select(&:last).map do |name, pairs|
|
67
|
+
values = pairs.map do |pair|
|
68
|
+
pair.values_at "code", "name"
|
69
|
+
end.to_h
|
70
|
+
[name, values]
|
71
|
+
end.to_h
|
72
|
+
[id, coded_values]
|
73
|
+
end.to_h
|
74
|
+
end
|
75
|
+
|
76
|
+
coded_values = fields.map do |field|
|
77
|
+
[field["name"], field.dig("domain", "codedValues")]
|
78
|
+
end.select(&:last).map do |name, pairs|
|
79
|
+
values = pairs.map do |pair|
|
80
|
+
pair.values_at "code", "name"
|
81
|
+
end.to_h
|
82
|
+
[name, values]
|
83
|
+
end.to_h
|
84
|
+
|
85
|
+
geometry = { rings: @map.bounding_box(margin).reproject_to(projection).coordinates.map(&:reverse) }.to_json
|
86
|
+
where = Array(where).map { |clause| "(#{clause})"}.join " AND "
|
87
|
+
query = { geometry: geometry, geometryType: "esriGeometryPolygon", returnIdsOnly: true, where: where }
|
88
|
+
|
89
|
+
object_ids = connection.get_json(query_path, query)["objectIds"]
|
90
|
+
next GeoJSON::Collection.new projection unless object_ids
|
91
|
+
|
92
|
+
features = Enumerator.new do |yielder|
|
93
|
+
per_page, total = [*per_page, *max_record_count, 500].min, object_ids.length
|
94
|
+
while object_ids.any?
|
95
|
+
yield total - object_ids.length, total if block_given? && total > 0
|
96
|
+
yielder << begin
|
97
|
+
connection.get_json query_path, outFields: ?*, objectIds: object_ids.take(per_page).join(?,)
|
98
|
+
rescue Error => error
|
99
|
+
(per_page /= 2) > 0 ? retry : raise(error)
|
100
|
+
end
|
101
|
+
object_ids.shift per_page
|
102
|
+
end
|
103
|
+
end.inject [] do |features, page|
|
104
|
+
features += page["features"]
|
105
|
+
end.map do |feature|
|
106
|
+
next unless geometry = feature["geometry"]
|
107
|
+
attributes = feature.fetch "attributes", {}
|
108
|
+
|
109
|
+
values = attributes.map do |name, value|
|
110
|
+
case
|
111
|
+
when type_id_field == name
|
112
|
+
type_values[value]
|
113
|
+
when decode = subtype_coded_values&.dig(attributes[type_id_field], name)
|
114
|
+
decode[value]
|
115
|
+
when decode = coded_values.dig(name)
|
116
|
+
decode[value]
|
117
|
+
when %w[null Null NULL <null> <Null> <NULL>].include?(value)
|
118
|
+
nil
|
119
|
+
else value
|
120
|
+
end
|
121
|
+
end
|
122
|
+
attributes = attributes.keys.zip(values).to_h
|
123
|
+
|
124
|
+
case geometry_type
|
125
|
+
when "esriGeometryPoint"
|
126
|
+
point = geometry.values_at "x", "y"
|
127
|
+
next unless point.all?
|
128
|
+
next GeoJSON::Point.new point, attributes
|
129
|
+
when "esriGeometryMultipoint"
|
130
|
+
points = geometry["points"]
|
131
|
+
next unless points&.any?
|
132
|
+
next GeoJSON::MultiPoint.new points.transpose.take(2).transpose, attributes
|
133
|
+
when "esriGeometryPolyline"
|
134
|
+
raise Error, "ArcGIS curve geometries not supported" if geometry.key? "curvePaths"
|
135
|
+
paths = geometry["paths"]
|
136
|
+
next unless paths&.any?
|
137
|
+
next GeoJSON::LineString.new paths[0], attributes if paths.one?
|
138
|
+
next GeoJSON::MultiLineString.new paths, attributes
|
139
|
+
when "esriGeometryPolygon"
|
140
|
+
raise Error, "ArcGIS curve geometries not supported" if geometry.key? "curveRings"
|
141
|
+
rings = geometry["rings"]
|
142
|
+
next unless rings&.any?
|
143
|
+
rings.each(&:reverse!) unless rings[0].anticlockwise?
|
144
|
+
next GeoJSON::Polygon.new rings, attributes if rings.one?
|
145
|
+
next GeoJSON::MultiPolygon.new rings.slice_before(&:anticlockwise?).to_a, attributes
|
146
|
+
else
|
147
|
+
raise Error, "unsupported ArcGIS geometry type: #{geometry_type}"
|
148
|
+
end
|
149
|
+
end.compact
|
150
|
+
|
151
|
+
GeoJSON::Collection.new projection, features
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module DEM
|
3
|
+
include GDALGlob, Log
|
4
|
+
|
5
|
+
def get_dem(temp_dir, dem_path)
|
6
|
+
txt_path = temp_dir / "dem.txt"
|
7
|
+
vrt_path = temp_dir / "dem.vrt"
|
8
|
+
te = @map.bounds(margin: margin).transpose.flatten
|
9
|
+
|
10
|
+
Dir.chdir(@source ? @source.parent : Pathname.pwd) do
|
11
|
+
log_update "%s: examining DEM" % @name
|
12
|
+
gdal_rasters @path do |index, total|
|
13
|
+
log_update "%s: examining DEM file %i of %i" % [@name, index, total]
|
14
|
+
end
|
15
|
+
end.tap do |rasters|
|
16
|
+
raise "no elevation data found at specified path" if rasters.none?
|
17
|
+
log_update "%s: extracting DEM raster" % @name
|
18
|
+
end.group_by do |path, info|
|
19
|
+
Projection.new info.dig("coordinateSystem", "wkt")
|
20
|
+
end.map.with_index do |(projection, rasters), index|
|
21
|
+
raise "DEM data not in planar projection with metre units" unless projection.proj4.split(?\s).any?("+units=m")
|
22
|
+
|
23
|
+
paths, resolutions = rasters.map do |path, info|
|
24
|
+
[path, info["geoTransform"].values_at(1, 2).norm]
|
25
|
+
end.sort_by(&:last).transpose
|
26
|
+
|
27
|
+
txt_path.write paths.reverse.join(?\n)
|
28
|
+
@resolution ||= resolutions.first
|
29
|
+
|
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, "-te", *te, "-tr", @resolution, @resolution, "-r", "bilinear", vrt_path, indexed_dem_path
|
33
|
+
indexed_dem_path
|
34
|
+
end.tap do |dem_paths|
|
35
|
+
txt_path.write dem_paths.join(?\n)
|
36
|
+
OS.gdalbuildvrt "-overwrite", "-input_file_list", txt_path, vrt_path
|
37
|
+
OS.gdal_translate vrt_path, dem_path
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def blur_dem(dem_path, blur_path)
|
42
|
+
sigma = @smooth * @map.scale / 1000.0
|
43
|
+
half = (3 * sigma / @resolution).ceil
|
44
|
+
|
45
|
+
coeffs = (-half..half).map do |n|
|
46
|
+
n * @resolution / sigma
|
47
|
+
end.map do |x|
|
48
|
+
Math::exp(-x**2)
|
49
|
+
end
|
50
|
+
|
51
|
+
vrt = OS.gdalbuildvrt "/vsistdout/", dem_path
|
52
|
+
xml = REXML::Document.new vrt
|
53
|
+
xml.elements.each("//ComplexSource") do |complex_source|
|
54
|
+
kernel_filtered_source = complex_source.parent.add_element("KernelFilteredSource")
|
55
|
+
complex_source.elements.each("SourceFilename|OpenOptions|SourceBand|SourceProperties|SrcRect|DstRect") do |element|
|
56
|
+
kernel_filtered_source.add_element element
|
57
|
+
end
|
58
|
+
kernel = kernel_filtered_source.add_element("Kernel", "normalized" => 1)
|
59
|
+
kernel.add_element("Size").text = coeffs.size
|
60
|
+
kernel.add_element("Coefs").text = coeffs.join ?\s
|
61
|
+
complex_source.parent.delete complex_source
|
62
|
+
end
|
63
|
+
|
64
|
+
log_update "%s: smoothing DEM raster" % @name
|
65
|
+
OS.gdal_translate "/vsistdin/", blur_path do |stdin|
|
66
|
+
stdin.write xml
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
class ESRIHdr
|
3
|
+
def initialize(path_or_object, *args)
|
4
|
+
@header = case path_or_object
|
5
|
+
when Pathname then path_or_object.sub_ext(".hdr").each_line.map(&:upcase).map(&:split).to_h
|
6
|
+
when ESRIHdr then path_or_object.header.dup
|
7
|
+
end
|
8
|
+
|
9
|
+
@format = case @header.values_at "PIXELTYPE", "NBITS", "BYTEORDER"
|
10
|
+
when %w[SIGNEDINT 8 I] then "c*"
|
11
|
+
when %w[SIGNEDINT 8 M] then "c*"
|
12
|
+
when %w[SIGNEDINT 16 I] then "s<*"
|
13
|
+
when %w[SIGNEDINT 16 M] then "s>*"
|
14
|
+
when %w[SIGNEDINT 32 I] then "l<*"
|
15
|
+
when %w[SIGNEDINT 32 M] then "l>*"
|
16
|
+
when %w[UNSIGNEDINT 8 I] then "C*"
|
17
|
+
when %w[UNSIGNEDINT 8 M] then "C*"
|
18
|
+
when %w[UNSIGNEDINT 16 I] then "S<*"
|
19
|
+
when %w[UNSIGNEDINT 16 M] then "S>*"
|
20
|
+
when %w[UNSIGNEDINT 32 I] then "L<*"
|
21
|
+
when %w[UNSIGNEDINT 32 M] then "L>*"
|
22
|
+
when %w[FLOAT 32 I] then "e*"
|
23
|
+
when %w[FLOAT 32 M] then "g*"
|
24
|
+
end
|
25
|
+
|
26
|
+
@nodata = case path_or_object
|
27
|
+
when Pathname
|
28
|
+
case @header.values_at "PIXELTYPE", "NBITS"
|
29
|
+
when %w[SIGNEDINT 8] then args.take(1).pack("c").unpack("c").first
|
30
|
+
when %w[UNSIGNEDINT 8] then args.take(1).pack("C").unpack("C").first
|
31
|
+
when %w[SIGNEDINT 16] then args.take(1).pack("s").unpack("s").first
|
32
|
+
when %w[UNSIGNEDINT 16] then args.take(1).pack("S").unpack("S").first
|
33
|
+
when %w[SIGNEDINT 32] then args.take(1).pack("l").unpack("l").first
|
34
|
+
when %w[UNSIGNEDINT 32] then args.take(1).pack("L").unpack("L").first
|
35
|
+
when %w[FLOAT 32] then args.first
|
36
|
+
else abort @header.inspect
|
37
|
+
end if args.any?
|
38
|
+
when ESRIHdr then path_or_object.nodata
|
39
|
+
end
|
40
|
+
|
41
|
+
@values = case path_or_object
|
42
|
+
when Pathname
|
43
|
+
path_or_object.sub_ext(".bil").binread.unpack(@format).map do |value|
|
44
|
+
value == @nodata ? nil : value
|
45
|
+
end
|
46
|
+
when ESRIHdr then args[0]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def write(path)
|
51
|
+
@header.map do |pair|
|
52
|
+
"%-#{@header.keys.map(&:length).max}s %s\n" % pair
|
53
|
+
end.join('').tap do |text|
|
54
|
+
path.sub_ext(".hdr").write text
|
55
|
+
end
|
56
|
+
@values.map do |value|
|
57
|
+
value || @nodata
|
58
|
+
end.pack(@format).tap do |data|
|
59
|
+
path.sub_ext(".bil").binwrite data
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
attr_reader :header, :values, :nodata
|
64
|
+
|
65
|
+
def nrows
|
66
|
+
@nrows ||= @header["NROWS"].to_i
|
67
|
+
end
|
68
|
+
|
69
|
+
def ncols
|
70
|
+
@ncols ||= @header["NCOLS"].to_i
|
71
|
+
end
|
72
|
+
|
73
|
+
def rows
|
74
|
+
@values.each_slice ncols
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module GDALGlob
|
3
|
+
def gdal_raster?(format)
|
4
|
+
@gdal_formats ||= OS.gdalinfo("--formats").each_line.drop(1).map do |line|
|
5
|
+
line.strip.split(?\s).first
|
6
|
+
end - %w[PDF]
|
7
|
+
@gdal_formats.include? format
|
8
|
+
end
|
9
|
+
|
10
|
+
def gdal_rasters(path)
|
11
|
+
paths = Array(path).flat_map do |path|
|
12
|
+
Pathname.glob Pathname(path).expand_path
|
13
|
+
end
|
14
|
+
|
15
|
+
total = nil
|
16
|
+
Enumerator.new do |yielder|
|
17
|
+
while path = paths.pop
|
18
|
+
OS.gdalmanage("identify", "-r", "-u", path).each_line.map do |line|
|
19
|
+
line.chomp.split ": "
|
20
|
+
end.each do |component, format|
|
21
|
+
case
|
22
|
+
when gdal_raster?(format) then yielder << component
|
23
|
+
when path == component
|
24
|
+
when component !~ /\.zip$/
|
25
|
+
else paths << "/vsizip/#{component}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end.entries.tap do |paths|
|
30
|
+
total = paths.length
|
31
|
+
end.map.with_index do |path, index|
|
32
|
+
yield [index + 1, total] if block_given?
|
33
|
+
info = JSON.parse OS.gdalinfo("-json", path)
|
34
|
+
next unless info["geoTransform"]
|
35
|
+
next unless wkt = info.dig("coordinateSystem", "wkt")
|
36
|
+
next path, info
|
37
|
+
rescue JSON::ParserError
|
38
|
+
end.compact
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module GeoJSON
|
3
|
+
class Collection
|
4
|
+
def initialize(projection = Projection.wgs84, features = [])
|
5
|
+
@projection, @features = projection, features
|
6
|
+
end
|
7
|
+
attr_reader :projection, :features
|
8
|
+
|
9
|
+
def self.load(json, projection = nil)
|
10
|
+
collection = JSON.parse(json)
|
11
|
+
proj4 = collection.dig "crs", "properties", "name"
|
12
|
+
projection ||= proj4 ? Projection.new(proj4) : Projection.wgs84
|
13
|
+
collection["features"].map do |feature|
|
14
|
+
geometry, properties = feature.values_at "geometry", "properties"
|
15
|
+
type, coordinates = geometry.values_at "type", "coordinates"
|
16
|
+
raise Error, "unsupported geometry type: #{type}" unless TYPES === type
|
17
|
+
GeoJSON.const_get(type).new coordinates, properties
|
18
|
+
end.yield_self do |features|
|
19
|
+
new projection, features
|
20
|
+
end
|
21
|
+
rescue JSON::ParserError
|
22
|
+
raise Error, "invalid GeoJSON data"
|
23
|
+
end
|
24
|
+
|
25
|
+
def <<(feature)
|
26
|
+
tap { @features << feature }
|
27
|
+
end
|
28
|
+
alias push <<
|
29
|
+
|
30
|
+
include Enumerable
|
31
|
+
def each(&block)
|
32
|
+
block_given? ? tap { @features.each(&block) } : @features.each
|
33
|
+
end
|
34
|
+
|
35
|
+
def reproject_to(projection)
|
36
|
+
return self if self.projection == projection
|
37
|
+
json = OS.ogr2ogr "-t_srs", projection, "-f", "GeoJSON", "-lco", "RFC7946=NO", "/vsistdout/", "GeoJSON:/vsistdin/" do |stdin|
|
38
|
+
stdin.puts to_json
|
39
|
+
end
|
40
|
+
Collection.load json, projection
|
41
|
+
end
|
42
|
+
|
43
|
+
def reproject_to_wgs84
|
44
|
+
reproject_to Projection.wgs84
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_h
|
48
|
+
{
|
49
|
+
"type" => "FeatureCollection",
|
50
|
+
"crs" => { "type" => "name", "properties" => { "name" => @projection } },
|
51
|
+
"features" => map(&:to_h)
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
extend Forwardable
|
56
|
+
delegate %i[coordinates properties] => :first
|
57
|
+
delegate %i[reject! select!] => :@features
|
58
|
+
|
59
|
+
def to_json(**extras)
|
60
|
+
to_h.merge(extras).to_json
|
61
|
+
end
|
62
|
+
|
63
|
+
def explode
|
64
|
+
Collection.new @projection, flat_map(&:explode)
|
65
|
+
end
|
66
|
+
|
67
|
+
def multi
|
68
|
+
Collection.new @projection, map(&:multi)
|
69
|
+
end
|
70
|
+
|
71
|
+
def merge(other)
|
72
|
+
raise Error, "can't merge different projections" unless @projection == other.projection
|
73
|
+
Collection.new @projection, @features + other.features
|
74
|
+
end
|
75
|
+
|
76
|
+
def merge!(other)
|
77
|
+
raise Error, "can't merge different projections" unless @projection == other.projection
|
78
|
+
tap { @features.concat other.features }
|
79
|
+
end
|
80
|
+
|
81
|
+
def clip!(hull)
|
82
|
+
@features.map! do |feature|
|
83
|
+
feature.clip hull
|
84
|
+
end.compact!
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
# TODO: what about empty collections?
|
89
|
+
def bounds
|
90
|
+
map(&:bounds).transpose.map(&:flatten).map(&:minmax)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module GeoJSON
|
3
|
+
class MultiLineString
|
4
|
+
include StraightSkeleton
|
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
|
+
def length
|
29
|
+
@coordinates.sum(&:path_length)
|
30
|
+
end
|
31
|
+
|
32
|
+
def offset(*margins, **options)
|
33
|
+
linestrings = margins.inject Nodes.new(@coordinates) do |nodes, margin|
|
34
|
+
nodes.progress limit: margin, **options.slice(:rounding_angle, :cutoff_angle)
|
35
|
+
end.readout
|
36
|
+
MultiLineString.new linestrings, @properties
|
37
|
+
end
|
38
|
+
|
39
|
+
def buffer(*margins, **options)
|
40
|
+
MultiLineString.new(@coordinates + @coordinates.map(&:reverse), @properties).offset(*margins, **options)
|
41
|
+
end
|
42
|
+
|
43
|
+
def smooth(margin, **options)
|
44
|
+
linestrings = Nodes.new(@coordinates).tap do |nodes|
|
45
|
+
nodes.progress **options.slice(:rounding_angle).merge(limit: margin)
|
46
|
+
nodes.progress **options.slice(:rounding_angle, :cutoff_angle).merge(limit: -2 * margin)
|
47
|
+
nodes.progress **options.slice(:rounding_angle, :cutoff_angle).merge(limit: margin)
|
48
|
+
end.readout
|
49
|
+
MultiLineString.new linestrings, @properties
|
50
|
+
end
|
51
|
+
|
52
|
+
def samples(interval)
|
53
|
+
points = @coordinates.map do |linestring|
|
54
|
+
distance = linestring.path_length
|
55
|
+
linestring.sample_at(interval, along: true).map do |point, along|
|
56
|
+
[point, (2 * along - distance).abs - distance]
|
57
|
+
end
|
58
|
+
end.flatten(1).sort_by(&:last).map(&:first)
|
59
|
+
MultiPoint.new points, @properties
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module GeoJSON
|
3
|
+
class MultiPoint
|
4
|
+
def clip(hull)
|
5
|
+
points = [hull, hull.perps].transpose.inject(@coordinates) do |result, (vertex, perp)|
|
6
|
+
result.select { |point| point.minus(vertex).dot(perp) >= 0 }
|
7
|
+
end
|
8
|
+
points.none? ? nil : points.one? ? Point.new(*points, @properties) : MultiPoint.new(points, @properties)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|