nswtopo 2.0.0 → 3.0.1
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 +232 -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 +112 -18
- 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 +204 -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 +7 -196
- 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
data/lib/nswtopo/map.rb
CHANGED
@@ -1,32 +1,34 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
class Map
|
3
|
-
include Formats, Dither, Zip, Log, Safely
|
4
|
-
|
5
|
-
def initialize(archive,
|
6
|
-
@archive, @
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
include Formats, Dither, Zip, Log, Safely, TiledWebMap
|
4
|
+
|
5
|
+
def initialize(archive, neatline:, centre:, dimensions:, scale:, rotation:, layers: {})
|
6
|
+
@archive, @neatline, @centre, @dimensions, @scale, @rotation, @layers = archive, neatline, centre, dimensions, scale, rotation, layers
|
7
|
+
params = { k_0: 1.0 / @scale, units: "mm", x_0: 0.0005 * @dimensions[0], y_0: 0.0005 * @dimensions[1] }
|
8
|
+
@projection = rotation.zero? ?
|
9
|
+
Projection.transverse_mercator(*centre, **params) :
|
10
|
+
Projection.oblique_mercator(*centre, alpha: rotation, **params)
|
11
|
+
@cutline = @neatline.reproject_to(@projection)
|
12
12
|
end
|
13
|
-
attr_reader :
|
13
|
+
attr_reader :centre, :dimensions, :scale, :rotation, :projection
|
14
14
|
|
15
15
|
extend Forwardable
|
16
|
-
delegate %i[write mtime read] => :@archive
|
16
|
+
delegate %i[write mtime read uptodate?] => :@archive
|
17
17
|
|
18
|
-
def self.init(archive, scale: 25000, rotation: 0.0, bounds: nil, coords: nil, dimensions: nil, margins: nil)
|
19
|
-
|
18
|
+
def self.init(archive, scale: 25000, rotation: 0.0, bounds: nil, coords: nil, dimensions: nil, inset: [], margins: nil)
|
19
|
+
points = case
|
20
|
+
when dimensions && margins
|
21
|
+
raise "can't specify both margins and map dimensions"
|
20
22
|
when coords && bounds
|
21
23
|
raise "can't specify both bounds file and map coordinates"
|
22
24
|
when coords
|
23
25
|
coords
|
24
26
|
when bounds
|
25
|
-
gps = GPS.load
|
27
|
+
gps = GPS.load(bounds).explode
|
26
28
|
margins ||= [15, 15] unless dimensions || gps.polygons.any?
|
27
29
|
case
|
28
30
|
when gps.polygons.any?
|
29
|
-
gps.polygons.
|
31
|
+
gps.polygons.flat_map(&:coordinates).inject(&:+)
|
30
32
|
when gps.linestrings.any?
|
31
33
|
gps.linestrings.map(&:coordinates).inject(&:+)
|
32
34
|
when gps.points.any?
|
@@ -37,64 +39,125 @@ module NSWTopo
|
|
37
39
|
else
|
38
40
|
raise "no bounds file or map coordinates specified"
|
39
41
|
end
|
42
|
+
margins ||= [0, 0]
|
40
43
|
|
41
|
-
|
42
|
-
|
44
|
+
centre = points.transpose.map(&:minmax).map(&:sum).times(0.5)
|
45
|
+
equidistant = Projection.azimuthal_equidistant *centre
|
43
46
|
|
44
47
|
case rotation
|
45
48
|
when "auto"
|
46
49
|
raise "can't specify both map dimensions and auto-rotation" if dimensions
|
47
|
-
|
48
|
-
|
50
|
+
local_points = GeoJSON.multipoint(points).reproject_to(equidistant).coordinates
|
51
|
+
local_centre, local_extents, rotation = local_points.minimum_bounding_box(*margins)
|
49
52
|
rotation *= -180.0 / Math::PI
|
50
53
|
when "magnetic"
|
51
|
-
rotation = declination(*
|
54
|
+
rotation = declination(*centre)
|
52
55
|
else
|
53
56
|
raise "map rotation must be between ±45°" unless rotation.abs <= 45
|
54
57
|
end
|
55
58
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
raise "can't specify both margins and map dimensions" if margins
|
60
|
-
extents = dimensions.map do |dimension|
|
61
|
-
dimension * 0.001 * scale
|
62
|
-
end
|
63
|
-
centre = GeoJSON.point(wgs84_centre).reproject_to(projection).coordinates
|
64
|
-
else
|
65
|
-
points = GeoJSON.multipoint(wgs84_points).reproject_to(projection).coordinates
|
66
|
-
centre, extents = points.map do |point|
|
59
|
+
unless dimensions || local_centre
|
60
|
+
local_points = GeoJSON.multipoint(points).reproject_to(equidistant).coordinates
|
61
|
+
local_centre, local_extents = local_points.map do |point|
|
67
62
|
point.rotate_by_degrees rotation
|
68
63
|
end.transpose.map(&:minmax).map do |min, max|
|
69
64
|
[0.5 * (max + min), max - min]
|
70
65
|
end.transpose
|
71
|
-
|
66
|
+
local_centre.rotate_by_degrees! -rotation
|
72
67
|
end
|
73
68
|
|
74
|
-
|
75
|
-
|
69
|
+
unless dimensions
|
70
|
+
dimensions = local_extents.times(1000.0 / scale).plus margins.times(2)
|
71
|
+
centre = GeoJSON.point(local_centre, projection: equidistant).reproject_to_wgs84.coordinates
|
72
|
+
end
|
76
73
|
|
77
|
-
|
78
|
-
|
79
|
-
|
74
|
+
params = { units: "mm", axis: "esu", k_0: 1.0 / scale, x_0: 0.0005 * dimensions[0], y_0: -0.0005 * dimensions[1] }
|
75
|
+
projection = rotation.zero? ?
|
76
|
+
Projection.transverse_mercator(*centre, **params) :
|
77
|
+
Projection.oblique_mercator(*centre, alpha: rotation, **params)
|
80
78
|
|
81
79
|
case
|
82
|
-
when
|
80
|
+
when dimensions.all?(&:positive?)
|
83
81
|
when coords
|
84
82
|
raise "not enough information to calculate map size – add more coordinates, or specify map dimensions or margins"
|
85
83
|
when bounds
|
86
84
|
raise "not enough information to calculate map size – check bounds file, or specify map dimensions or margins"
|
87
85
|
end
|
88
86
|
|
89
|
-
|
87
|
+
insets = inset.map do |inset|
|
88
|
+
inset.each_slice(2).entries.transpose.map(&:sort)
|
89
|
+
end.each.with_object GeoJSON::Collection.new(projection: projection, name: "insets") do |bounds, collection|
|
90
|
+
dimensions.zip(bounds).each do |dimension, (min, max)|
|
91
|
+
raise OptionParser::InvalidArgument, "inset falls outside map dimensions" unless max > 0 && min < dimension
|
92
|
+
end
|
93
|
+
collection.add_polygon [bounds.inject(&:product).values_at(0,2,3,1,0)]
|
94
|
+
end
|
95
|
+
|
96
|
+
neatline = if insets.any?
|
97
|
+
OS.ogr2ogr *%w[-f GeoJSON -lco RFC7946=NO /vsistdout/ GeoJSON:/vsistdin/ -dialect sqlite -sql], <<~SQL do |stdin|
|
98
|
+
SELECT ST_Difference(BuildMbr(0,0,#{dimensions.join ?,}), ST_Union(geometry)) AS geometry
|
99
|
+
FROM insets
|
100
|
+
SQL
|
101
|
+
stdin.puts insets.to_json
|
102
|
+
end.then do |json|
|
103
|
+
GeoJSON::Collection.load(json, projection: projection, name: "neatline").explode
|
104
|
+
end
|
105
|
+
else
|
106
|
+
ring = [[0, 0], dimensions].transpose.inject(&:product).values_at(0,2,3,1,0)
|
107
|
+
GeoJSON.polygon [ring], projection: projection, name: "neatline"
|
108
|
+
end
|
109
|
+
|
110
|
+
raise OptionParser::InvalidArgument, "inset covers map" if neatline.none?
|
111
|
+
raise OptionParser::InvalidArgument, "inset creates non-contiguous map" unless neatline.one?
|
112
|
+
new(archive, neatline: neatline, centre: centre, dimensions: dimensions, scale: scale, rotation: rotation).save
|
90
113
|
end
|
91
114
|
|
92
115
|
def self.load(archive)
|
93
|
-
|
116
|
+
properties = YAML.load(archive.read "map.yml")
|
117
|
+
neatline = GeoJSON::Collection.load(archive.read "map.json")
|
118
|
+
new archive, neatline: neatline, **properties
|
119
|
+
rescue ArgumentError, YAML::Exception, GeoJSON::Error
|
120
|
+
raise NSWTopo::Archive::Invalid
|
94
121
|
end
|
95
122
|
|
96
123
|
def save
|
97
|
-
tap
|
124
|
+
tap do
|
125
|
+
write "map.json", @neatline.to_json
|
126
|
+
write "map.yml", YAML.dump(centre: @centre, dimensions: @dimensions, scale: @scale, rotation: @rotation, layers: @layers)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.from_svg(archive, svg_path)
|
131
|
+
xml = REXML::Document.new(svg_path.read)
|
132
|
+
|
133
|
+
unless false == Config["versioning"]
|
134
|
+
creator_tool = xml.elements["svg/metadata/rdf:RDF/rdf:Description[@xmp:CreatorTool]/@xmp:CreatorTool"]&.value
|
135
|
+
version = Version[creator_tool]
|
136
|
+
raise "SVG nswtopo version too old: %s" % svg_path unless version >= MIN_VERSION
|
137
|
+
raise "SVG nswtopo version too new: %s" % svg_path unless version <= VERSION
|
138
|
+
end
|
139
|
+
|
140
|
+
/^0\s+0\s+(?<width>\S+)\s+(?<height>\S+)$/ =~ xml.elements["svg[@viewBox]/@viewBox"]&.value
|
141
|
+
width && xml.elements["svg[ @width='#{ width}mm']"] || raise(Version::Error)
|
142
|
+
height && xml.elements["svg[@height='#{height}mm']"] || raise(Version::Error)
|
143
|
+
dimensions = [width, height].map(&:to_f)
|
144
|
+
|
145
|
+
metadata = xml.elements["svg/metadata/nswtopo:map[@projection][@centre][@scale][@rotation]"] || raise(Version::Error)
|
146
|
+
projection = Projection.new metadata.attributes["projection"]
|
147
|
+
neatline = GeoJSON.polygon JSON.parse(metadata.attributes["neatline"]), projection: projection
|
148
|
+
centre = JSON.parse metadata.attributes["centre"]
|
149
|
+
scale = metadata.attributes["scale"].to_i
|
150
|
+
rotation = metadata.attributes["rotation"].to_f
|
151
|
+
|
152
|
+
new(archive, neatline: neatline, centre: centre, dimensions: dimensions, scale: scale, rotation: rotation).save.tap do |map|
|
153
|
+
map.write "map.svg", svg_path.read
|
154
|
+
end
|
155
|
+
rescue Version::Error, JSON::ParserError
|
156
|
+
raise "not an nswtopo SVG file: %s" % svg_path
|
157
|
+
rescue SystemCallError
|
158
|
+
raise "couldn't read file: %s" % svg_path
|
159
|
+
rescue REXML::ParseException
|
160
|
+
raise "unrecognised map file: %s" % svg_path
|
98
161
|
end
|
99
162
|
|
100
163
|
def layers
|
@@ -103,78 +166,63 @@ module NSWTopo
|
|
103
166
|
end
|
104
167
|
end
|
105
168
|
|
106
|
-
def
|
107
|
-
|
108
|
-
ppi ||= 0.0254 * @scale / resolution
|
109
|
-
return (@extents / resolution).map(&:ceil), ppi, resolution
|
169
|
+
def neatline(mm: nil)
|
170
|
+
mm ? @neatline.buffer(mm).explode : @neatline
|
110
171
|
end
|
111
172
|
|
112
|
-
def
|
113
|
-
|
173
|
+
def cutline(mm: nil)
|
174
|
+
mm ? @cutline.buffer(mm).explode : @cutline
|
114
175
|
end
|
115
176
|
|
116
|
-
def
|
117
|
-
|
118
|
-
ring = @extents.map do |extent|
|
119
|
-
[-0.5 * extent - margin, 0.5 * extent + margin]
|
120
|
-
end.inject(&:product).map do |offset|
|
121
|
-
@centre.plus offset.rotate_by_degrees(-@rotation)
|
122
|
-
end.values_at(0,2,3,1,0)
|
123
|
-
GeoJSON.polygon [ring], projection: projection
|
177
|
+
def te
|
178
|
+
[0, 0, *@dimensions]
|
124
179
|
end
|
125
180
|
|
126
|
-
def
|
127
|
-
|
128
|
-
projection ? bbox.reproject_to(projection) : bbox
|
129
|
-
end.coordinates.first.transpose.map(&:minmax)
|
181
|
+
def to_mm(metres)
|
182
|
+
metres * 1000.0 / @scale
|
130
183
|
end
|
131
184
|
|
132
|
-
def
|
133
|
-
|
185
|
+
def to_metres(mm)
|
186
|
+
mm * @scale / 1000.0
|
134
187
|
end
|
135
188
|
|
136
|
-
def
|
137
|
-
|
138
|
-
|
139
|
-
WorldFile.write top_left, resolution, -@rotation, path
|
189
|
+
def geotransform(resolution: nil, ppi: nil)
|
190
|
+
mm_per_px = ppi ? 25.4 / ppi : to_mm(resolution)
|
191
|
+
[0.0, mm_per_px, 0.0, @dimensions[1], 0.0, -mm_per_px]
|
140
192
|
end
|
141
193
|
|
142
|
-
def
|
143
|
-
|
144
|
-
|
194
|
+
def write_world_file(path, **opts)
|
195
|
+
ulx, mm_per_px, _, uly, _, _ = geotransform(**opts)
|
196
|
+
path.open("w") do |file|
|
197
|
+
file.puts mm_per_px, 0, 0, -mm_per_px
|
198
|
+
file.puts ulx + 0.5 * mm_per_px
|
199
|
+
file.puts uly - 0.5 * mm_per_px
|
145
200
|
end
|
146
201
|
end
|
147
202
|
|
148
|
-
def
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
end
|
154
|
-
end.each_line.map do |line|
|
155
|
-
line.split(?\s).take(2).map(&:to_f)
|
203
|
+
def write_pam_file(path, **opts)
|
204
|
+
REXML::Document.new("", raw: %w[SRS], attribute_quote: :quote).add_element("PAMDataset").tap do |pam|
|
205
|
+
pam.add_element("SRS", "dataAxisToSRSAxisMapping" => "1,2").add_text @projection.wkt2
|
206
|
+
pam.add_element("GeoTransform").add_text geotransform(**opts).join(?,)
|
207
|
+
path.write pam
|
156
208
|
end
|
157
|
-
metre_diagonal.distance / pixel_diagonal.distance
|
158
|
-
rescue OS::Error
|
159
|
-
raise "invalid raster"
|
160
209
|
end
|
161
210
|
|
162
211
|
def self.declination(longitude, latitude)
|
163
212
|
today = Date.today
|
164
|
-
query = {
|
165
|
-
uri = URI::HTTPS.build host: "
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
rescue RuntimeError, SystemCallError, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, SocketError
|
213
|
+
query = { latd: latitude, lond: longitude, latm: 0, lonm: 0, lats: 0, lons: 0, elev: 0, year: today.year, month: today.month, day: today.day, Ein: "Dtrue" }
|
214
|
+
uri = URI::HTTPS.build host: "api.geomagnetism.ga.gov.au", path: "/agrf", query: URI.encode_www_form(query)
|
215
|
+
json = Net::HTTP.get uri
|
216
|
+
Float(JSON.parse(json).dig("magneticFields", "D").to_s.sub(/ .*/, ""))
|
217
|
+
rescue JSON::ParserError, ArgumentError, TypeError, SystemCallError, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, SocketError
|
170
218
|
raise "couldn't get magnetic declination value"
|
171
219
|
end
|
172
220
|
|
173
221
|
def declination
|
174
|
-
Map.declination
|
222
|
+
Map.declination *@centre
|
175
223
|
end
|
176
224
|
|
177
|
-
def add(*layers, after: nil, before: nil, replace: nil, overwrite: false)
|
225
|
+
def add(*layers, after: nil, before: nil, replace: nil, overwrite: false, strict: false)
|
178
226
|
[%w[before after replace], [before, after, replace]].transpose.select(&:last).each do |option, name|
|
179
227
|
next if self.layers.any? { |other| other.name == name }
|
180
228
|
raise "no such layer: %s" % name
|
@@ -182,6 +230,7 @@ module NSWTopo
|
|
182
230
|
raise OptionParser::AmbiguousOption, "can't specify --%s and --%s simultaneously" % options
|
183
231
|
end
|
184
232
|
|
233
|
+
strict ||= layers.one?
|
185
234
|
layers.inject [self.layers, false, replace || after, []] do |(layers, changed, follow, errors), layer|
|
186
235
|
index = layers.index layer unless replace || after || before
|
187
236
|
if overwrite || !layer.uptodate?
|
@@ -203,15 +252,28 @@ module NSWTopo
|
|
203
252
|
index = layers.index { |other| (other <=> layer) > 0 } || -1
|
204
253
|
end
|
205
254
|
next layers.insert(index, layer), true, layer.name, errors
|
206
|
-
rescue
|
207
|
-
log_warn
|
255
|
+
rescue ArcGIS::Connection::Error => error
|
256
|
+
log_warn "couldn't download layer: #{layer.name}"
|
208
257
|
next layers, changed, follow, errors << error
|
258
|
+
rescue RuntimeError => error
|
259
|
+
errors << error
|
260
|
+
break layers, changed, follow, errors if strict
|
261
|
+
log_warn error.message
|
262
|
+
next layers, changed, follow, errors
|
209
263
|
end.tap do |ordered_layers, changed, follow, errors|
|
210
264
|
if changed
|
211
265
|
@layers.replace Hash[ordered_layers.map(&:pair)]
|
212
266
|
replace ? delete(replace) : save
|
213
267
|
end
|
214
|
-
|
268
|
+
case
|
269
|
+
when errors.none?
|
270
|
+
when strict
|
271
|
+
raise errors.first
|
272
|
+
when errors.one?
|
273
|
+
raise PartialFailureError, "failed to create layer"
|
274
|
+
else
|
275
|
+
raise PartialFailureError, "failed to create #{errors.length} layers"
|
276
|
+
end
|
215
277
|
end
|
216
278
|
end
|
217
279
|
|
@@ -231,37 +293,68 @@ module NSWTopo
|
|
231
293
|
save
|
232
294
|
end
|
233
295
|
|
234
|
-
def
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
296
|
+
def move(name, before: nil, after: nil)
|
297
|
+
name, target = [name, before || after].map do |name|
|
298
|
+
Layer.sanitise name
|
299
|
+
end.each do |name|
|
300
|
+
raise OptionParser::InvalidArgument, "no such layer: #{name}" unless @layers.key? name
|
301
|
+
end
|
302
|
+
raise OptionParser::InvalidArgument, "layers must be different" if name == target
|
303
|
+
insert = [name, @layers.delete(name)]
|
304
|
+
@layers.each.with_object [] do |(name, layer), layers|
|
305
|
+
layers << insert if before && name == target
|
306
|
+
layers << [name, layer]
|
307
|
+
layers << insert if after && name == target
|
308
|
+
end.tap do |layers|
|
309
|
+
@layers.replace layers.to_h
|
310
|
+
end
|
311
|
+
save
|
312
|
+
end
|
313
|
+
|
314
|
+
def info(empty: nil, json: false, proj: false, wkt: false)
|
315
|
+
case
|
316
|
+
when json
|
317
|
+
bbox = @neatline.reproject_to_wgs84.first
|
318
|
+
bbox.properties.merge! dimensions: @dimensions, scale: @scale, rotation: @rotation, layers: layers.map(&:name)
|
319
|
+
JSON.pretty_generate bbox.to_h
|
320
|
+
when proj
|
321
|
+
OS.gdalsrsinfo("-o", "proj4", "--single-line", @projection)
|
322
|
+
when wkt
|
323
|
+
OS.gdalsrsinfo("-o", "wkt2", @projection).gsub(/\n\n+|\A\n+/, "")
|
324
|
+
else
|
325
|
+
area_km2 = @neatline.area * (0.000001 * @scale)**2
|
326
|
+
extents_km = @dimensions.times(0.000001 * @scale)
|
327
|
+
StringIO.new.tap do |io|
|
328
|
+
io.puts "%-11s 1:%i" % ["scale:", @scale]
|
329
|
+
io.puts "%-11s %imm × %imm" % ["dimensions:", *@dimensions.map(&:round)]
|
330
|
+
io.puts "%-11s %.1fkm × %.1fkm" % ["extent:", *extents_km]
|
331
|
+
io.puts "%-11s %.1fkm²" % ["area:", area_km2]
|
332
|
+
io.puts "%-11s %.1f°" % ["rotation:", @rotation]
|
333
|
+
layers.reject(&empty ? :nil? : :empty?).inject("layers:") do |heading, layer|
|
334
|
+
io.puts "%-11s %s" % [heading, layer]
|
335
|
+
nil
|
336
|
+
end
|
337
|
+
end.string
|
338
|
+
end
|
246
339
|
end
|
247
340
|
alias to_s info
|
248
341
|
|
249
|
-
def render(*paths, worldfile: false, force: false,
|
342
|
+
def render(*paths, worldfile: false, force: false, background: nil, **options)
|
250
343
|
@archive.delete "map.svg" if force
|
251
344
|
Dir.mktmppath do |temp_dir|
|
252
345
|
rasters = Hash.new do |rasters, opts|
|
253
346
|
png_path = temp_dir / "raster.#{rasters.size}.png"
|
254
|
-
|
255
|
-
rasterise png_path,
|
256
|
-
|
347
|
+
pam_path = temp_dir / "raster.#{rasters.size}.png.aux.xml"
|
348
|
+
rasterise png_path, background: background, **opts
|
349
|
+
write_pam_file pam_path, **opts
|
257
350
|
rasters[opts] = png_path
|
258
351
|
end
|
259
352
|
dithers = Hash.new do |dithers, opts|
|
260
353
|
png_path = temp_dir / "dither.#{dithers.size}.png"
|
261
|
-
|
354
|
+
pam_path = temp_dir / "dither.#{dithers.size}.png.aux.xml"
|
262
355
|
FileUtils.cp rasters[opts], png_path
|
263
356
|
dither png_path
|
264
|
-
|
357
|
+
write_pam_file pam_path, **opts
|
265
358
|
dithers[opts] = png_path
|
266
359
|
end
|
267
360
|
|
@@ -269,7 +362,7 @@ module NSWTopo
|
|
269
362
|
ext = path.extname.delete_prefix ?.
|
270
363
|
name = path.basename(path.extname)
|
271
364
|
out_path = temp_dir / "output.#{index}.#{ext}"
|
272
|
-
send "render_#{ext}", out_path, name: name,
|
365
|
+
send "render_#{ext}", out_path, name: name, background: background, **options do |dither: false, **opts|
|
273
366
|
(dither ? dithers : rasters)[opts]
|
274
367
|
end
|
275
368
|
next out_path, path
|
@@ -279,6 +372,8 @@ module NSWTopo
|
|
279
372
|
outputs.each do |out_path, path|
|
280
373
|
FileUtils.cp out_path, path
|
281
374
|
log_success "created %s" % path
|
375
|
+
rescue SystemCallError
|
376
|
+
raise "couldn't save #{path}"
|
282
377
|
end
|
283
378
|
|
284
379
|
paths.select do |path|
|
data/lib/nswtopo/os.rb
CHANGED
@@ -6,54 +6,32 @@ module NSWTopo
|
|
6
6
|
GDAL = %w[
|
7
7
|
gdal_contour
|
8
8
|
gdal_grid
|
9
|
-
gdal_rasterize
|
10
9
|
gdal_translate
|
11
|
-
gdaladdo
|
12
10
|
gdalbuildvrt
|
13
11
|
gdaldem
|
14
|
-
gdalenhance
|
15
12
|
gdalinfo
|
16
13
|
gdallocationinfo
|
17
14
|
gdalmanage
|
18
|
-
gdalserver
|
19
15
|
gdalsrsinfo
|
20
|
-
gdaltindex
|
21
16
|
gdaltransform
|
22
17
|
gdalwarp
|
23
|
-
gnmanalyse
|
24
|
-
gnmmanage
|
25
|
-
nearblack
|
26
18
|
ogr2ogr
|
27
19
|
ogrinfo
|
28
|
-
ogrlineref
|
29
|
-
ogrtindex
|
30
|
-
testepsg
|
31
|
-
]
|
32
|
-
ImageMagick = %w[
|
33
|
-
animate
|
34
|
-
compare
|
35
|
-
composite
|
36
|
-
conjure
|
37
|
-
convert
|
38
|
-
display
|
39
|
-
identify
|
40
|
-
import
|
41
|
-
mogrify
|
42
|
-
montage
|
43
|
-
stream
|
44
20
|
]
|
21
|
+
ImageMagick = %w[magick]
|
45
22
|
SQLite3 = %w[sqlite3]
|
46
23
|
PNGQuant = %w[pngquant]
|
47
24
|
GIMP = %w[gimp]
|
48
25
|
Zip = %w[zip]
|
49
26
|
SevenZ = %w[7z]
|
27
|
+
ExifTool = %w[exiftool]
|
50
28
|
|
51
29
|
extend self
|
52
30
|
|
53
|
-
%w[GDAL ImageMagick SQLite3 PNGQuant GIMP Zip SevenZ].each do |package|
|
31
|
+
%w[GDAL ImageMagick SQLite3 PNGQuant GIMP Zip SevenZ ExifTool].each do |package|
|
54
32
|
OS.const_get(package).each do |command|
|
55
|
-
define_method command do |*args, &block|
|
56
|
-
Open3.popen3 command, *args.map(&:to_s) do |stdin, stdout, stderr, thread|
|
33
|
+
define_method command do |*args, **opts, &block|
|
34
|
+
Open3.popen3 command, *args.map(&:to_s), **opts do |stdin, stdout, stderr, thread|
|
57
35
|
thr_in = Thread.new do
|
58
36
|
block.call(stdin) if block
|
59
37
|
rescue Errno::EPIPE
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
module TiledWebMap
|
3
|
+
HALF, TILE_SIZE, DEFAULT_ZOOM = Math::PI * 6378137, 256, 16
|
4
|
+
|
5
|
+
def tiled_web_map(temp_dir, extension:, zoom: [DEFAULT_ZOOM], **options, &block)
|
6
|
+
web_mercator_bounds = @cutline.reproject_to(Projection.new("EPSG:3857")).bounds
|
7
|
+
wgs84_bounds = @cutline.reproject_to_wgs84.bounds
|
8
|
+
|
9
|
+
png_path = nil
|
10
|
+
max_zoom, min_zoom = *zoom.sort.reverse
|
11
|
+
max_zoom.downto(0).map do |zoom|
|
12
|
+
indices, ts = web_mercator_bounds.map do |lower, upper|
|
13
|
+
(2**zoom * (lower + HALF) / HALF / 2).floor ... (2**zoom * (upper + HALF) / HALF / 2).ceil
|
14
|
+
end.map do |indices|
|
15
|
+
[indices, indices.size * TILE_SIZE]
|
16
|
+
end.transpose
|
17
|
+
te = [*indices.map(&:begin), *indices.map(&:end)].map do |index|
|
18
|
+
index * 2 * HALF / 2**zoom - HALF
|
19
|
+
end
|
20
|
+
resolution = 2 * HALF / TILE_SIZE / 2**zoom
|
21
|
+
tif_path = temp_dir / "tile.#{zoom}.tif"
|
22
|
+
OpenStruct.new resolution: resolution, ts: ts, te: te, tif_path: tif_path, indices: indices, zoom: zoom
|
23
|
+
end.select do |level|
|
24
|
+
next true if level.zoom == max_zoom
|
25
|
+
next level.zoom >= min_zoom if min_zoom
|
26
|
+
!level.indices.all?(&:one?)
|
27
|
+
end.tap do |max_level, *|
|
28
|
+
png_path = yield(resolution: max_level.resolution)
|
29
|
+
end.tap do |levels|
|
30
|
+
log_update "#{extension}: creating zoom levels %s" % levels.map(&:zoom).minmax.uniq.join(?-)
|
31
|
+
end.each.concurrently do |level|
|
32
|
+
OS.gdalwarp "-t_srs", "EPSG:3857", "-ts", *level.ts, "-te", *level.te, "-r", "cubic", "-dstalpha", png_path, level.tif_path
|
33
|
+
end.flat_map do |level|
|
34
|
+
cols, rows = level.indices
|
35
|
+
[cols.each, rows.reverse_each].map(&:with_index).map(&:entries).inject(&:product).map do |(col, j), (row, i)|
|
36
|
+
row ^= 2**level.zoom - 1 if extension == "gemf"
|
37
|
+
path = temp_dir / "tile.#{level.zoom}.#{col}.#{row}.png"
|
38
|
+
args = ["-srcwin", j * TILE_SIZE, i * TILE_SIZE, TILE_SIZE, TILE_SIZE, level.tif_path, path]
|
39
|
+
OpenStruct.new zoom: level.zoom, row: row, col: col, path: path, args: args
|
40
|
+
end
|
41
|
+
end.tap do |tiles|
|
42
|
+
log_update "#{extension}: creating %i tiles" % tiles.length
|
43
|
+
end.each.concurrently do |tile|
|
44
|
+
OS.gdal_translate *tile.args
|
45
|
+
end.entries.tap do |tiles|
|
46
|
+
log_update "#{extension}: optimising %i tiles" % tiles.length
|
47
|
+
tiles.map(&:path).each.concurrent_groups do |paths|
|
48
|
+
dither *paths
|
49
|
+
rescue Dither::Missing
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module NSWTopo
|
2
|
+
class TreeIndenter
|
3
|
+
def initialize(items, parts = nil, &block)
|
4
|
+
@enum = Enumerator.new do |yielder|
|
5
|
+
next unless items
|
6
|
+
grouped = block ? block.(items) : items
|
7
|
+
grouped.each.with_index do |(item, group), index|
|
8
|
+
*new_parts, last_part = parts
|
9
|
+
case last_part
|
10
|
+
when "├─ " then new_parts << "│ "
|
11
|
+
when "└─ " then new_parts << " "
|
12
|
+
end
|
13
|
+
new_parts << case index
|
14
|
+
when grouped.size - 1 then "└─ "
|
15
|
+
else "├─ "
|
16
|
+
end if parts
|
17
|
+
yielder << [new_parts, item]
|
18
|
+
TreeIndenter.new(group, new_parts, &block).inject(yielder, &:<<)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
extend Forwardable
|
24
|
+
include Enumerable
|
25
|
+
delegate :each => :@enum
|
26
|
+
end
|
27
|
+
end
|
data/lib/nswtopo/version.rb
CHANGED
@@ -1,4 +1,29 @@
|
|
1
1
|
module NSWTopo
|
2
|
-
|
3
|
-
|
2
|
+
class Version
|
3
|
+
include Comparable
|
4
|
+
Error = Class.new StandardError
|
5
|
+
|
6
|
+
def self.[](creator_string)
|
7
|
+
/^nswtopo (?<digit_string>\d+(\.\d+(\.\d+)?)?)$/ =~ creator_string.to_s
|
8
|
+
digit_string ? new(digit_string) : raise(Error)
|
9
|
+
end
|
10
|
+
|
11
|
+
def creator_string
|
12
|
+
"nswtopo #{self}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(digit_string)
|
16
|
+
@to_s = digit_string
|
17
|
+
@to_a = digit_string.split(?.).map(&:to_i)
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :to_s, :to_a
|
21
|
+
|
22
|
+
def <=>(other)
|
23
|
+
self.to_a <=> other.to_a
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
VERSION = Version["nswtopo 3.0.1"]
|
28
|
+
MIN_VERSION = Version["nswtopo 3.0"]
|
4
29
|
end
|