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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/COPYING +70 -83
  3. data/bin/nswtopo +227 -116
  4. data/docs/README.md +1 -12
  5. data/docs/add.md +1 -1
  6. data/docs/config.md +1 -1
  7. data/docs/contours.md +3 -1
  8. data/docs/init.md +8 -0
  9. data/docs/inspect.md +103 -0
  10. data/docs/move.md +9 -0
  11. data/docs/render.md +16 -7
  12. data/docs/scrape.md +67 -0
  13. data/docs/spot-heights.md +6 -2
  14. data/lib/nswtopo/archive.rb +50 -41
  15. data/lib/nswtopo/chrome.rb +227 -0
  16. data/lib/nswtopo/commands/add.rb +106 -0
  17. data/lib/nswtopo/commands/config.rb +38 -0
  18. data/lib/nswtopo/commands/inspect.rb +74 -0
  19. data/lib/nswtopo/commands/layers.rb +22 -0
  20. data/lib/nswtopo/commands/scrape.rb +79 -0
  21. data/lib/nswtopo/commands.rb +57 -0
  22. data/lib/nswtopo/dither.rb +5 -3
  23. data/lib/nswtopo/font.rb +46 -21
  24. data/lib/nswtopo/formats/gemf.rb +42 -0
  25. data/lib/nswtopo/formats/kmz.rb +26 -24
  26. data/lib/nswtopo/formats/mbtiles.rb +5 -41
  27. data/lib/nswtopo/formats/pdf.rb +82 -17
  28. data/lib/nswtopo/formats/svg.rb +114 -45
  29. data/lib/nswtopo/formats/svgz.rb +2 -2
  30. data/lib/nswtopo/formats/zip.rb +33 -23
  31. data/lib/nswtopo/formats.rb +77 -32
  32. data/lib/nswtopo/geometry/overlap.rb +1 -32
  33. data/lib/nswtopo/geometry/r_tree.rb +16 -10
  34. data/lib/nswtopo/geometry/segment.rb +3 -3
  35. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +5 -6
  36. data/lib/nswtopo/geometry/vector_sequence.rb +7 -6
  37. data/lib/nswtopo/gis/arcgis/connection.rb +56 -0
  38. data/lib/nswtopo/gis/arcgis/layer/map.rb +163 -0
  39. data/lib/nswtopo/gis/arcgis/layer/query.rb +87 -0
  40. data/lib/nswtopo/gis/arcgis/layer/renderer.rb +66 -0
  41. data/lib/nswtopo/gis/arcgis/layer/statistics.rb +15 -0
  42. data/lib/nswtopo/gis/arcgis/layer.rb +201 -0
  43. data/lib/nswtopo/gis/arcgis/service.rb +57 -0
  44. data/lib/nswtopo/gis/arcgis.rb +3 -0
  45. data/lib/nswtopo/gis/dem.rb +13 -12
  46. data/lib/nswtopo/gis/esri_hdr.rb +8 -2
  47. data/lib/nswtopo/gis/geojson/collection.rb +45 -21
  48. data/lib/nswtopo/gis/geojson/multi_line_string.rb +2 -24
  49. data/lib/nswtopo/gis/geojson/multi_polygon.rb +2 -53
  50. data/lib/nswtopo/gis/geojson/polygon.rb +15 -0
  51. data/lib/nswtopo/gis/geojson.rb +12 -3
  52. data/lib/nswtopo/gis/gps/kml.rb +25 -19
  53. data/lib/nswtopo/gis/gps.rb +2 -0
  54. data/lib/nswtopo/gis/projection.rb +35 -24
  55. data/lib/nswtopo/gis/shapefile.rb +89 -16
  56. data/lib/nswtopo/gis.rb +1 -2
  57. data/lib/nswtopo/helpers/array.rb +0 -11
  58. data/lib/nswtopo/helpers/colour.rb +34 -14
  59. data/lib/nswtopo/layer/arcgis_raster.rb +44 -48
  60. data/lib/nswtopo/layer/colour_mask.rb +5 -0
  61. data/lib/nswtopo/layer/contour.rb +35 -28
  62. data/lib/nswtopo/layer/control.rb +2 -7
  63. data/lib/nswtopo/layer/declination.rb +9 -9
  64. data/lib/nswtopo/layer/feature.rb +36 -22
  65. data/lib/nswtopo/layer/grid.rb +30 -27
  66. data/lib/nswtopo/layer/import.rb +1 -21
  67. data/lib/nswtopo/layer/labels/barrier.rb +39 -0
  68. data/lib/nswtopo/layer/labels.rb +551 -383
  69. data/lib/nswtopo/layer/mask_render.rb +37 -0
  70. data/lib/nswtopo/layer/overlay.rb +2 -2
  71. data/lib/nswtopo/layer/raster.rb +31 -41
  72. data/lib/nswtopo/layer/raster_import.rb +17 -0
  73. data/lib/nswtopo/layer/raster_render.rb +15 -0
  74. data/lib/nswtopo/layer/relief.rb +27 -95
  75. data/lib/nswtopo/layer/spot.rb +63 -62
  76. data/lib/nswtopo/layer/vector/cutout.rb +15 -0
  77. data/lib/nswtopo/layer/vector/knockout.rb +16 -0
  78. data/lib/nswtopo/layer/vector.rb +121 -89
  79. data/lib/nswtopo/layer/vegetation.rb +39 -34
  80. data/lib/nswtopo/layer.rb +30 -16
  81. data/lib/nswtopo/map.rb +202 -109
  82. data/lib/nswtopo/os.rb +5 -27
  83. data/lib/nswtopo/tiled_web_map.rb +54 -0
  84. data/lib/nswtopo/tree_indenter.rb +27 -0
  85. data/lib/nswtopo/version.rb +27 -2
  86. data/lib/nswtopo.rb +6 -199
  87. metadata +41 -22
  88. data/lib/nswtopo/font/chrome.rb +0 -59
  89. data/lib/nswtopo/font/generic.rb +0 -25
  90. data/lib/nswtopo/gis/arcgis_server/connection.rb +0 -52
  91. data/lib/nswtopo/gis/arcgis_server.rb +0 -155
  92. data/lib/nswtopo/gis/geojson/multi_point.rb +0 -12
  93. data/lib/nswtopo/gis/world_file.rb +0 -19
  94. data/lib/nswtopo/layer/labels/fence.rb +0 -20
data/bin/nswtopo CHANGED
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Copyright 2011-2019 Matthew Hollingworth
3
+ # Copyright 2011-2022 Matthew Hollingworth
4
4
  #
5
5
  # This program is free software: you can redistribute it and/or modify
6
- # it under the terms of the GNU General Public License as published by
6
+ # it under the terms of the GNU Affero General Public License as published by
7
7
  # the Free Software Foundation, either version 3 of the License, or
8
8
  # (at your option) any later version.
9
9
  #
10
10
  # This program is distributed in the hope that it will be useful,
11
11
  # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
12
  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- # GNU General Public License for more details.
13
+ # GNU Affero General Public License for more details.
14
14
  #
15
- # You should have received a copy of the GNU General Public License
16
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+ # You should have received a copy of the GNU Affero General Public License
16
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  begin
19
19
  require 'open3'
@@ -30,75 +30,80 @@ begin
30
30
 
31
31
  GDAL_VERSION = begin
32
32
  stdout, * = Open3.capture3 "gdalinfo", "--version"
33
- stdout[/\d+(?:\.\d+){1,2}/]
34
- rescue Errno::ENOENT
33
+ /^GDAL (?<version>\d+(?:\.\d+){1,2})/ =~ stdout
34
+ version || raise
35
+ rescue Errno::ENOENT, RuntimeError
35
36
  log_abort "GDAL not installed"
36
37
  end
37
38
 
38
39
  case
39
- when (RUBY_VERSION.split(/\D+/).take(3).map(&:to_i) <=> [2,5]) < 0
40
- log_abort "ruby 2.5 or greater required"
40
+ when (RUBY_VERSION.split(/\D+/).take(3).map(&:to_i) <=> [3,0,4]) < 0
41
+ log_abort "ruby 3.0.4 or greater required"
41
42
  when !Zlib.const_defined?(:GzipFile)
42
43
  log_abort "ruby with GZIP_SUPPORT required"
43
- when (GDAL_VERSION.split(/\D+/).take(3).map(&:to_i) <=> [2,3]) < 0
44
- log_abort "GDAL 2.3 or greater required"
44
+ when (GDAL_VERSION.split(/\D+/).take(3).map(&:to_i) <=> [3,4]) < 0
45
+ log_abort "GDAL 3.4 or greater required"
45
46
  end
46
47
 
47
48
  digits = '\d+(?:_\d+)*'
48
49
  float = "[-+]?(?:#{digits}(?=(.)?)(?:\\.(?:#{digits})?)?|\\.#{digits})(?:[eE][-+]?#{digits})?"
49
- coords = "#{float},#{float}"
50
-
51
- PositiveInt = /\A#{digits}\z/
52
- PositiveFloat = /\A#{float}\z/
53
- NonNegFloat = 0..Float::INFINITY
54
- Dimensions = /\A#{float},#{float}\z/
55
- Margins = /\A#{float}(?:,#{float})?\z/
56
- CoordList = /\A#{coords}(?:,#{coords})*\z/
57
- Rotation = /\A(?:#{float}|magnetic|auto)\z/
58
- AltitudeAngle = 0..90
59
- Opacity = /\A#{float}%?\z/
60
- DashArray = /\A#{float}(?:(?:,#{float})*|(?: #{float})*)\z/
61
-
62
- OptionParser.accept PositiveInt, PositiveInt do |string|
50
+ pair = "#{float},#{float}"
51
+ quad = "#{float},#{float},#{float},#{float}"
52
+ field = '[^\s",]+'
53
+
54
+ PositiveInt = Class.new
55
+ PositiveFloat = Class.new
56
+ NonNegFloat = Class.new
57
+ Dimensions = Class.new
58
+ Inset = Class.new
59
+ Margins = Class.new
60
+ CoordList = Class.new
61
+ Rotation = Class.new
62
+ Opacity = Class.new
63
+ DashArray = Class.new
64
+ Zoom = Class.new
65
+ ZlibLevel = Class.new
66
+ FieldList = Class.new
67
+
68
+ OptionParser.accept PositiveInt, /\A#{digits}\z/ do |string|
63
69
  raise OptionParser::InvalidArgument, string unless string.to_i.positive?
64
70
  string.to_i
65
71
  end
66
72
 
67
- OptionParser.accept PositiveFloat, PositiveFloat do |string|
73
+ OptionParser.accept PositiveFloat, /\A#{float}\z/ do |string|
68
74
  raise OptionParser::InvalidArgument, string unless string.to_f.positive?
69
75
  string.to_f
70
76
  end
71
77
 
72
- OptionParser.accept NonNegFloat, PositiveFloat do |string|
78
+ OptionParser.accept NonNegFloat, /\A#{float}\z/ do |string|
73
79
  raise OptionParser::InvalidArgument, string if string.to_f.negative?
74
80
  string.to_f
75
81
  end
76
82
 
77
- OptionParser.accept Dimensions, Dimensions do |string|
83
+ OptionParser.accept Dimensions, /\A#{pair}\z/ do |string|
78
84
  dimensions = string.split(?,).map(&:to_f)
79
85
  raise OptionParser::InvalidArgument, string unless dimensions.all?(&:positive?)
80
86
  dimensions
81
87
  end
82
88
 
83
- OptionParser.accept Margins, Margins do |string|
89
+ OptionParser.accept Inset, /\A#{quad}(?:,#{quad})*\z/ do |string|
90
+ string.split(?,).map(&:to_f).each_slice(4).entries
91
+ end
92
+
93
+ OptionParser.accept Margins, /\A#{float}(?:,#{float})?\z/ do |string|
84
94
  margins = string.split(?,).map(&:to_f)
85
95
  raise OptionParser::InvalidArgument, string if margins.any?(&:negative?)
86
96
  margins.one? ? margins * 2 : margins
87
97
  end
88
98
 
89
- OptionParser.accept CoordList, CoordList do |string|
99
+ OptionParser.accept CoordList, /\A#{pair}(?:,#{pair})*\z/ do |string|
90
100
  string.split(?,).map(&:to_f).each_slice(2).to_a
91
101
  end
92
102
 
93
- OptionParser.accept Rotation, Rotation do |string|
103
+ OptionParser.accept Rotation, /\A(?:#{float}|magnetic|auto)\z/ do |string|
94
104
  "magnetic" == string ? string : "auto" == string ? string : string.to_f
95
105
  end
96
106
 
97
- OptionParser.accept AltitudeAngle, PositiveFloat do |string|
98
- raise OptionParser::InvalidArgument, string unless AltitudeAngle === string.to_f
99
- string.to_f
100
- end
101
-
102
107
  OptionParser.accept Pathname do |string|
103
108
  path = Pathname(string).expand_path
104
109
  raise OptionParser::InvalidArgument, string unless path.exist?
@@ -106,23 +111,37 @@ begin
106
111
  end
107
112
 
108
113
  OptionParser.accept Colour do |string|
109
- string == "none" ? string : Colour.new(string.downcase).to_s
114
+ string == "none" ? string : Colour.new(string.downcase)
110
115
  rescue Colour::Error
111
116
  raise OptionParser::InvalidArgument, string
112
117
  end
113
118
 
114
- OptionParser.accept Opacity, Opacity do |string|
119
+ OptionParser.accept Opacity, /\A#{float}%?\z/ do |string|
115
120
  opacity = string.end_with?(?%) ? Float(string.chomp ?%) * 0.01 : Float(string)
116
121
  raise OptionParser::InvalidArgument, string unless (0..1) === opacity
117
122
  opacity
118
123
  end
119
124
 
120
- OptionParser.accept DashArray, DashArray do |string|
125
+ OptionParser.accept DashArray, /\A#{float}(?:(?:,#{float})*|(?: #{float})*)\z/ do |string|
121
126
  values = string.split(/[, ]/).map(&:to_f)
122
127
  raise OptionParser::InvalidArgument, string if values.any?(&:negative?)
123
128
  values.join ?\s
124
129
  end
125
130
 
131
+ OptionParser.accept Zoom, /\A(?:#{digits},)?#{digits}\z/ do |string|
132
+ values = string.split(?,).map(&:to_i)
133
+ raise OptionParser::InvalidArgument, string unless values.all?(10..20)
134
+ values
135
+ end
136
+
137
+ OptionParser.accept ZlibLevel, /\A\d\z/ do |string|
138
+ Integer(string)
139
+ end
140
+
141
+ OptionParser.accept FieldList, /\A#{field}(?:,#{field})*\z/ do |string|
142
+ string.split ?,
143
+ end
144
+
126
145
  ansi = lambda do |string|
127
146
  string.to_s.gsub(/\*([-a-zA-Z0-9]+)\*/) do
128
147
  "\e[1m%s\e[0m" % $1
@@ -130,7 +149,7 @@ begin
130
149
  "\e[4m%s\e[0m" % $1
131
150
  end.gsub(/~([-a-zA-Z0-9]+)~/) do
132
151
  "\e[3m%s\e[0m" % $1
133
- end
152
+ end.prepend("\r\e[K")
134
153
  end
135
154
 
136
155
  plain = lambda do |string|
@@ -154,6 +173,7 @@ begin
154
173
  controls add rogaine control markers
155
174
  overlay add KML or GPX overlay
156
175
  delete delete map layer
176
+ move move map layer
157
177
  render render map in various formats
158
178
  layers list available map layers
159
179
  config configure nswtopo
@@ -188,15 +208,16 @@ begin
188
208
  *nswtopo* *init* - initialise map bounds and scale
189
209
  usage: _nswtopo_ _init_ [~options~] <map.tgz>
190
210
  EOF
191
- parser.on "-s", "--scale <scale>", PositiveInt, "scale of map (default 25000)"
192
- parser.on "-b", "--bounds <bounds.kml>", Pathname, "bounds for map as KML or GPX file"
193
- parser.on "-c", "--coords <x1,y1,...>", CoordList, "bounds for map as one or more WGS84",
194
- "longitude/latitude pairs"
195
- parser.on "-d", "--dimensions <width,height>", Dimensions, "dimensions of map in mm"
196
- parser.on "-m", "--margins <x[,y]>", Margins, "map margins in mm"
197
- parser.on "-r", "--rotation <rotation>", Rotation, "map rotation angle in clockwise",
198
- "degrees, 'auto' or 'magnetic'"
199
- parser.on "-o", "--overwrite", "overwrite existing map file"
211
+ parser.on "-s", "--scale <scale>", PositiveInt, "scale of map (default 25000)"
212
+ parser.on "-b", "--bounds <bounds.kml>", Pathname, "bounds for map as KML or GPX file"
213
+ parser.on "-c", "--coords <x1,y1,...>", CoordList, "bounds for map as one or more WGS84",
214
+ "longitude/latitude pairs"
215
+ parser.on "-d", "--dimensions <width,height>", Dimensions, "map dimensions in mm"
216
+ parser.on "-r", "--rotation <rotation>", Rotation, "map rotation angle in clockwise",
217
+ "degrees, 'auto' or 'magnetic'"
218
+ parser.on "-m", "--margins <x[,y]>", Margins, "map margins in mm"
219
+ parser.on "-i", "--inset <x1,y1,x2,y2>", Inset, "map inset coordinates in mm"
220
+ parser.on "-o", "--overwrite", "overwrite existing map file"
200
221
 
201
222
  when "info"
202
223
  parser.banner = <<~EOF
@@ -204,72 +225,84 @@ begin
204
225
  usage: _nswtopo_ _info_ [~options~] <map.tgz>
205
226
  EOF
206
227
  parser.on "-e", "--empty", "show empty layers"
228
+ parser.on "-j", "--json", "show map bounds as GeoJSON string"
229
+ parser.on "-p", "--proj", "show map projection as a proj string"
230
+ parser.on "-w", "--wkt", "show map projection as a WKT2 string"
207
231
 
208
232
  when "add"
209
233
  parser.banner = <<~EOF
210
234
  *nswtopo* *add* - add named map layer
211
235
  usage: _nswtopo_ _add_ [~options~] <map.tgz> <layer> [<layer> ...]
212
236
  EOF
213
- parser.on "-r", "--resolution <resolution>", PositiveFloat, "raster layer resolution in metres"
237
+ parser.on "-r", "--resolution <resolution>", PositiveFloat, "raster resolution in metres per pixel"
238
+ parser.on "--ppi <ppi>", PositiveFloat, "raster resolution in pixels per inch"
239
+ parser.on "-o", "--opacity <opacity>", Opacity, "layer opacity (between 0 and 1)"
214
240
  parser.on "-p", "--path <path>", Pathname, "source data path for layer"
215
241
  parser.on "-a", "--after <layer>", "insert after specified layer"
216
242
  parser.on "-b", "--before <layer>", "insert before specified layer"
217
243
  parser.on "-c", "--replace <layer>", "replace specified layer"
218
244
  parser.on "-o", "--overwrite", "overwrite layer if it already exists"
245
+ parser.on "-s", "--strict", "don't continue if a layer fails"
219
246
 
220
247
  when "contours"
221
248
  parser.banner = <<~EOF
222
249
  *nswtopo* *contours* - add contours from elevation data
223
250
  usage: _nswtopo_ _contours_ [~options~] <map.tgz> <dem.zip>
224
251
  EOF
225
- parser.on "-i", "--interval <interval>", PositiveInt, "contour interval in metres (default %s)" % NSWTopo::Contour::DEFAULTS["interval"]
226
- parser.on "-x", "--index <index>", PositiveInt, "index interval in metres"
227
- parser.on "-s", "--smooth <radius>", NonNegFloat, "DEM smoothing radius in mm (default %s)" % NSWTopo::Contour::DEFAULTS["smooth"]
228
- parser.on "-t", "--thin", "thin intermediate contours in steep areas"
229
- # parser.on "-d", "--density <density>", PositiveFloat, "maximum lines/mm before thinning occurs"
230
- # parser.on "-m", "--min-length <length>", PositiveFloat, "minimum length before contour thinning in mm"
231
- parser.on "--no-depression", "don't show or clean depression contours"
232
- parser.on "-k", "--knolls <size>", NonNegFloat, "minimum knoll size in mm (default %s)" % NSWTopo::Contour::DEFAULTS["knolls"]
233
- parser.on "-a", "--after <layer>", "insert after specified layer"
234
- parser.on "-b", "--before <layer>", "insert before specified layer"
235
- parser.on "-c", "--replace <layer>", "replace specified layer"
236
- parser.on "--stroke <colour>", Colour, "stroke colour (name or RGB triplet)"
237
- parser.on "--stroke-width <width>", PositiveFloat, "stroke width in mm"
238
- parser.on "--fill <colour>", Colour, "label colour (defaults to stroke colour)"
252
+ parser.on "-i", "--interval <interval>", PositiveInt, "contour interval in metres (default %s)" % NSWTopo::Contour::DEFAULTS["interval"]
253
+ parser.on "-x", "--index <index>", PositiveInt, "index interval in metres"
254
+ parser.on "-a", "--auxiliary", "show auxiliary contours"
255
+ parser.on "-s", "--smooth <radius>", NonNegFloat, "DEM smoothing radius in mm (default %s)" % NSWTopo::Contour::DEFAULTS["smooth"]
256
+ parser.on "-t", "--thin", "thin intermediate contours in steep areas"
257
+ parser.on "-d", "--density <density>", PositiveFloat, "thinning threshold (default %s lines/mm)" % NSWTopo::Contour::DEFAULTS["density"]
258
+ # parser.on "-m", "--min-length <length>", PositiveFloat, "minimum length before contour thinning in mm"
259
+ parser.on "--no-depression", "don't show or clean depression contours"
260
+ parser.on "-k", "--knolls <size>", NonNegFloat, "minimum knoll size in mm (default %s)" % NSWTopo::Contour::DEFAULTS["knolls"]
261
+ parser.on "-a", "--after <layer>", "insert after specified layer"
262
+ parser.on "-b", "--before <layer>", "insert before specified layer"
263
+ parser.on "-c", "--replace <layer>", "replace specified layer"
264
+ parser.on "--stroke <colour>", Colour, "stroke colour (name or RGB triplet)"
265
+ parser.on "--stroke-width <width>", PositiveFloat, "stroke width in mm (default %s)" % NSWTopo::Contour::DEFAULTS["stroke-width"]
266
+ parser.on "--fill <colour>", Colour, "label colour (defaults to stroke colour)"
267
+ parser.on "-r", "--resolution <resolution>", PositiveFloat, "DEM processing resolution in metres"
239
268
 
240
269
  when "spot-heights"
241
270
  parser.banner = <<~EOF
242
271
  *nswtopo* *spot-heights* - add spot heights from elevation data
243
272
  usage: _nswtopo_ _spot-heights_ [~options~] <map.tgz> <dem.zip>
244
273
  EOF
245
- parser.on "-s", "--smooth <radius>", NonNegFloat, "DEM smoothing radius in mm (default %s)" % NSWTopo::Spot::DEFAULTS["smooth"]
246
- parser.on "--spacing <spacing>", PositiveFloat, "minimum spot spacing in mm (default %i)" % NSWTopo::Spot::DEFAULTS["spacing"]
247
- parser.on "-p", "--prefer <knolls|saddles>", %w[knolls saddles], "preferred spot locations"
248
- parser.on "-a", "--after <layer>", "insert after specified layer"
249
- parser.on "-b", "--before <layer>", "insert before specified layer"
250
- parser.on "-c", "--replace <layer>", "replace specified layer"
274
+ parser.on "-s", "--smooth <radius>", NonNegFloat, "DEM smoothing radius in mm (default %s)" % NSWTopo::Spot::DEFAULTS["smooth"]
275
+ parser.on "--spacing <spacing>", PositiveFloat, "minimum spot spacing in mm (default %i)" % NSWTopo::Spot::DEFAULTS["spacing"]
276
+ parser.on "-p", "--prefer <knolls|saddles>", %w[knolls saddles], "preferred spot locations"
277
+ parser.on "-e", "--extent <extent>", PositiveFloat, "minimum feature extent in mm (default %i)" % NSWTopo::Spot::DEFAULTS["extent"]
278
+ parser.on "-a", "--after <layer>", "insert after specified layer"
279
+ parser.on "-b", "--before <layer>", "insert before specified layer"
280
+ parser.on "-c", "--replace <layer>", "replace specified layer"
281
+ parser.on "-r", "--resolution <resolution>", PositiveFloat, "DEM processing resolution in metres"
251
282
 
252
283
  when "relief"
253
284
  parser.banner = <<~EOF
254
285
  *nswtopo* *relief* - add shaded relief
255
286
  usage: _nswtopo_ _relief_ [~options~] <map.tgz> <dem.zip>
256
287
  EOF
257
- parser.on "-r", "--resolution <resolution>", PositiveFloat, "resolution in metres (default %i)" % NSWTopo::Relief::DEFAULTS["resolution"]
258
- parser.on "-o", "--opacity <opacity>", Opacity, "opacity (default %s)" % NSWTopo::Relief::DEFAULTS["opacity"]
259
- parser.on "-a", "--altitude <altitude>", AltitudeAngle, "altitude angle in degrees (default %i)" % NSWTopo::Relief::DEFAULTS["altitude"]
260
- parser.on "-z", "--azimuth <azimuth>", Float, "azimuth in degrees (default %i)" % NSWTopo::Relief::DEFAULTS["azimuth"]
261
- parser.on "-s", "--sources <sources>", PositiveInt, "number of light sources (default %i)" % NSWTopo::Relief::DEFAULTS["sources"]
262
- parser.on "-y", "--yellow <fraction>", Opacity, "yellow illumination as a fraction",
263
- "of shading (default %s)" % NSWTopo::Relief::DEFAULTS["yellow"]
264
- parser.on "-f", "--factor <factor>", PositiveFloat, "exaggeration factor (default %s)" % NSWTopo::Relief::DEFAULTS["factor"]
288
+ parser.on "-r", "--resolution <resolution>", PositiveFloat, "resolution in metres per pixel"
289
+ parser.on "-p", "--ppi <ppi>", PositiveFloat, "resolution in pixels per inch"
290
+ parser.on "-o", "--opacity <opacity>", Opacity, "opacity (default %s)" % NSWTopo::Relief::DEFAULTS["opacity"]
291
+ parser.on "--shade <colour>", Colour, "shade colour (default %s)" % NSWTopo::Relief::DEFAULTS["shade"]
292
+ parser.on "-m", "--method <igor|combined>", %w[igor combined], "relief shading method (default %s)" % NSWTopo::Relief::DEFAULTS["method"]
293
+ parser.on "-z", "--azimuth <azimuth>", Float, "azimuth in degrees (default %i)" % NSWTopo::Relief::DEFAULTS["azimuth"]
294
+ parser.on "-f", "--factor <factor>", PositiveFloat, "exaggeration factor (default %s)" % NSWTopo::Relief::DEFAULTS["factor"]
265
295
 
266
296
  when "grid"
267
297
  parser.banner = <<~EOF
268
298
  *nswtopo* *grid* - add UTM grid
269
299
  usage: _nswtopo_ _grid_ [~options~] <map.tgz>
270
300
  EOF
271
- parser.on "-i", "--interval <interval>", PositiveFloat, "interval between grid lines in",
272
- "metres (default %i)" % NSWTopo::Grid::DEFAULTS["interval"]
301
+ parser.on "-i", "--interval <interval>", PositiveFloat, "interval between grid lines in",
302
+ "metres (default %i)" % NSWTopo::Grid::DEFAULTS["interval"]
303
+ parser.on "-u", "--unlabeled", "don't add grid labels"
304
+ parser.on "-b", "--border", "add map border"
305
+ parser.on "--stroke-width <width>", PositiveFloat, "stroke width in mm (default %s)" % NSWTopo::Grid::DEFAULTS["stroke-width"]
273
306
 
274
307
  when "declination"
275
308
  parser.banner = <<~EOF
@@ -292,6 +325,7 @@ begin
292
325
  parser.on "-s", "--spot", "add spots at centres"
293
326
  parser.on "-c", "--colour <colour>", Colour, "colour of markers and labels",
294
327
  "(name or RGB triplet)"
328
+ parser.on "-k", "--knockout <width>", NonNegFloat, "symbol knockout width in mm (default %s)" % NSWTopo::Control::DEFAULTS["knockout"]
295
329
  parser.on "-f", "--font-size <font-size>", PositiveFloat, "font size for labels in mm"
296
330
 
297
331
  when "overlay"
@@ -321,22 +355,30 @@ begin
321
355
  usage: _nswtopo_ _delete_ [~options~] <map.tgz> <layer> [<layer> ...]
322
356
  EOF
323
357
 
358
+ when "move"
359
+ parser.banner = <<~EOF
360
+ *nswtopo* *move* - move map layer
361
+ usage: _nswtopo_ _move_ [~options~] <map.tgz> <layer>
362
+ EOF
363
+ parser.on "-a", "--after <layer>", "insert after specified layer"
364
+ parser.on "-b", "--before <layer>", "insert before specified layer"
365
+
324
366
  when "render"
325
367
  parser.banner = <<~EOF
326
368
  *nswtopo* *render* - render map in various formats
327
- usage: _nswtopo_ _render_ [~options~] <map.tgz> [<format-or-path> ...]
369
+ usage: _nswtopo_ _render_ [~options~] <map.tgz|map.svg> [<format-or-path> ...]
328
370
  formats: #{NSWTopo::Formats.extensions.sort.join ?\s}
329
371
  default: svg
330
372
  EOF
331
- parser.on "-p", "--ppi <ppi>", PositiveInt, "resolution for raster formats in pixels",
332
- "per inch (default %i)" % NSWTopo::Formats::PPI
333
- parser.on "-z", "--zoom <zoom>", Integer, "maximum mbtiles zoom level (default %i)" % NSWTopo::Formats::Mbtiles::ZOOM
334
- parser.on "-d", "--dither", "use indexed colour for raster formats"
335
- parser.on "-w", "--worldfile", "save additional projection (.prj) and",
336
- "world file (.wld) for raster formats"
337
- parser.on "-o", "--overwrite", "overwrite existing output files"
338
- parser.on "-e", "--external <map.svg>", Pathname, "render from externally edited SVG"
339
- parser.on "-f", "--force", "force regeneration of cached SVG"
373
+ parser.on "-p", "--ppi <ppi>", PositiveInt, "resolution for raster formats in pixels",
374
+ "per inch (default %i)" % NSWTopo::Formats::PPI
375
+ parser.on "-z", "--zoom <zoom>", Zoom, "maximum tile zoom (10-20, default %i)" % NSWTopo::TiledWebMap::DEFAULT_ZOOM
376
+ parser.on "-b", "--background <colour>", Colour, "background colour (name or RGB triplet)"
377
+ parser.on "-d", "--dither", "use indexed colour for raster formats"
378
+ parser.on "-w", "--worldfile", "save additional projection (.prj) and",
379
+ "world file (.wld) for raster formats"
380
+ parser.on "-o", "--overwrite", "overwrite existing output files"
381
+ parser.on "-f", "--force", "force regeneration of cached SVG"
340
382
 
341
383
  when "layers"
342
384
  parser.banner = <<~EOF
@@ -351,11 +393,46 @@ begin
351
393
  EOF
352
394
  parser.on "-d", "--delete <name>", "delete configuration setting"
353
395
  parser.on "-c", "--chrome <path>", Pathname, "set path for Google Chrome"
354
- parser.on "-f", "--firefox <path>", Pathname, "set path for Firefox"
355
396
  parser.on "-p", "--path <path>", Pathname, "set path for given layer"
356
397
  parser.on "-r", "--resolution <resolution>", PositiveFloat, "set resolution for given layer"
357
398
  parser.on "--layer-dir <path>", Pathname, "set an extra layer directory"
358
399
  parser.on "--[no-]labelling", "enable or disable map labelling"
400
+ parser.on "--[no-]debug", "enable or disable label debuging"
401
+ parser.on "--[no-]gpu", "enable or disable Chrome GPU usage"
402
+ parser.on "--[no-]versioning", "enable or disable map version checking"
403
+ parser.on "-z", "--zlib-level <0-9>", ZlibLevel, "set zlib compression level"
404
+ parser.on "-k", "--knockout <width>", NonNegFloat, "set label knockout width in mm"
405
+
406
+ when "scrape"
407
+ parser.banner = <<~EOF
408
+ *nswtopo* *scrape* - scrape data from an ArcGIS REST endpoint
409
+ usage: _nswtopo_ _scrape_ [~options~] <url> <path>
410
+ EOF
411
+ parser.on "-l", "--layer <layer>", String, "name of ArcGIS service layer"
412
+ parser.on "-i", "--id <id>", Integer, "id number of layer"
413
+ parser.on "-w", "--where <where>", String, "filtering clause"
414
+ parser.on "-c", "--coords <x0,y0,x1,y1>", CoordList, "WGS84 coordinates of bounding box corners"
415
+ parser.on "-n", "--name <name>", /^\w+$/, "name of saved layer"
416
+ parser.on "-f", "--fields <field,...>", FieldList, "comma-separated list of fields"
417
+ parser.on "-d", "--decode", "convert coded values where possible"
418
+ parser.on "-e", "--epsg <number>", PositiveInt, "EPSG number for reprojection"
419
+ parser.on "-p", "--paginate <number>", PositiveInt, "number of records per request"
420
+ parser.on "--concat", "collect all features before saving"
421
+ parser.on "-u", "--unique <field>", String, "field for counting map-only features"
422
+
423
+ when "inspect"
424
+ parser.banner = <<~EOF
425
+ *nswtopo* *inspect* - inspect data from an ArcGIS REST endpoint or local data source
426
+ usage: _nswtopo_ inspect [~options~] <url-or-path>
427
+ EOF
428
+ parser.on "-l", "--layer <layer>", String, "name of layer"
429
+ parser.on "-i", "--id <id>", Integer, "id number of layer"
430
+ parser.on "-w", "--where <where>", String, "filtering clause"
431
+ parser.on "-c", "--coords <x0,y0,x1,y1>", CoordList, "WGS84 coordinates of bounding box corners"
432
+ parser.on "-f", "--fields <field,...>", FieldList, "comma-separated list of fields"
433
+ parser.on "-d", "--decode", "convert coded values where possible"
434
+ parser.on "--codes", "show coded values for an ArcGIS Layer"
435
+ parser.on "--countwise", "sort fields by count instead of value"
359
436
 
360
437
  when nil
361
438
  raise OptionParser::MissingArgument, "no command specified"
@@ -381,50 +458,84 @@ begin
381
458
  case command
382
459
  when "layers"
383
460
  raise OptionParser::NeedlessArgument, ARGV if ARGV.any?
384
- NSWTopo.layers options
461
+ NSWTopo.layers **options
385
462
  exit
386
463
  when "config"
387
464
  layer = ARGV.shift
388
465
  raise OptionParser::NeedlessArgument, ARGV if ARGV.any?
389
466
  NSWTopo.config *layer, **options
390
467
  exit
468
+ when "scrape"
469
+ url, path = ARGV.shift, ARGV.shift
470
+ raise OptionParser::MissingArgument, "no URL specified" unless url
471
+ raise OptionParser::MissingArgument, "no path specified" unless path
472
+ raise OptionParser::NeedlessArgument, ARGV if ARGV.any?
473
+ path = Pathname(path).expand_path
474
+ raise OptionParser::InvalidArgument, "invalid path: #{path}" unless path.parent.directory?
475
+ raise OptionParser::InvalidOption, "can't specify both --id and --layer" if options[:id] && options[:layer]
476
+ NSWTopo.scrape url, path, **options
477
+ exit
478
+ when "inspect"
479
+ url_or_path = ARGV.shift
480
+ raise OptionParser::MissingArgument, "no URL or path specified" unless url_or_path
481
+ raise OptionParser::NeedlessArgument, ARGV if ARGV.any?
482
+ [%i[where codes], %i[fields codes], %i[decode codes], %i[id layer]].each do |flags|
483
+ raise OptionParser::InvalidOption, "can't have --%s with --%s" % flags if options.values_at(*flags).all?
484
+ end
485
+ raise OptionParser::InvalidOption, "--countwise requires --fields" if options[:countwise] && !options[:fields]
486
+ raise OptionParser::InvalidOption, "--decode requires --fields" if options[:decode] && !options[:fields]
487
+ NSWTopo.inspect url_or_path, **options
488
+ exit
489
+ when "add", "relief"
490
+ raise OptionParser::InvalidOption, "can't specify both --resolution and --ppi" if options[:resolution] && options[:ppi]
391
491
  end
392
492
 
393
493
  raise OptionParser::MissingArgument, "no map path specified" if ARGV.empty?
394
494
  tgz_path = Pathname(ARGV.shift)
395
495
 
396
- begin
397
- in_path = case command
398
- when "init"
399
- raise "#{tgz_path} already exists" if !options.delete(:overwrite) && tgz_path.exist?
400
- else
401
- raise "no such file #{tgz_path}" unless tgz_path.exist?
402
- raise "#{tgz_path} is not a file" unless tgz_path.file?
403
- tgz_path
404
- end
405
-
406
- command = command.tr ?-, ?_
407
- arity, error = NSWTopo.method(command).arity, nil
496
+ in_path = case command
497
+ when "init"
498
+ raise "already a directory: #{tgz_path}" if tgz_path.directory?
499
+ raise "file already exists: #{tgz_path}" if !options.delete(:overwrite) && tgz_path.exist?
500
+ raise "no such directory: #{tgz_path.parent}" unless tgz_path.parent.directory?
501
+ else
502
+ raise "no such file: #{tgz_path}" unless tgz_path.exist?
503
+ raise "not a file: #{tgz_path}" unless tgz_path.file?
504
+ tgz_path
505
+ end
408
506
 
409
- NSWTopo::Archive.open(tgz_path, *in_path) do |archive|
410
- args = [archive, *ARGV, options]
411
- case
412
- when arity >= 0 && args.length > arity
413
- raise OptionParser::NeedlessArgument, ARGV.last(args.length - arity).join(?\s)
414
- when arity >= 0 ? args.length < arity : args.length + arity + 1 < 0
415
- raise OptionParser::MissingArgument
507
+ command = command.tr ?-, ?_
508
+ ARGV.prepend in_path.basename(".tgz").basename(".tar.gz").basename(".svg") if "render" == command
509
+
510
+ nil.tap do |partial_failure|
511
+ NSWTopo::Archive.open(in_path: in_path, out_path: tgz_path) do |archive|
512
+ NSWTopo.method(command).parameters.group_by(&:first).inject [archive] do |args, (type, params)|
513
+ case type
514
+ when :req
515
+ raise OptionParser::MissingArgument if ARGV.length < params.length - args.length
516
+ args.concat ARGV.shift(params.length - args.length)
517
+ when :rest
518
+ args.concat ARGV.shift(ARGV.length)
519
+ else args
520
+ end
521
+ end.tap do |args|
522
+ raise OptionParser::NeedlessArgument, ARGV.join(?\s) if ARGV.any?
523
+ NSWTopo.send command, *args, **options
524
+ rescue NSWTopo::PartialFailureError => partial_failure
416
525
  end
417
-
418
- NSWTopo.send command, *args
419
- rescue NSWTopo::PartialFailureError => error
420
526
  end
421
- raise error.message if error
527
+ raise partial_failure if partial_failure
528
+ rescue NSWTopo::Archive::Invalid
529
+ raise "unrecognised map file: #{in_path}" unless "render" == command
530
+ raise OptionParser::InvalidOption, "can't specify --force without a map file" if options[:force]
531
+ in_path, tgz_path, options = nil, nil, options.merge(svg_path: in_path)
532
+ retry
422
533
  end
423
534
  rescue OptionParser::ParseError => error
424
535
  warn ansi[command_parser] if $stderr.tty?
425
536
  log_abort error.message
426
537
  rescue Interrupt
427
- abort $stderr.tty? ? "\r\e[K\e[31mnswtopo:\e[0m interrupted" : "nswtopo: interrupted"
538
+ log_abort "interrupted"
428
539
  rescue RuntimeError => error
429
540
  log_abort error.message
430
541
  end
data/docs/README.md CHANGED
@@ -10,20 +10,9 @@ Help screens are available describing usage for each commands. Use the `--help`
10
10
  $ nswtopo init --help
11
11
  ```
12
12
 
13
- # Configuration
14
-
15
- An important initial step is to configure the location of *Google Chrome* on your PC. Chrome is required for rendering the map in most formats. Use the *configure* command to set the path:
16
-
17
- ```
18
- $ nswtopo config --chrome "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
19
- nswtopo: configuration updated
20
- ```
21
-
22
- Use forward slashes for paths, even on Windows.
23
-
24
13
  # Map Files
25
14
 
26
- Most commands need a map file to work on. Name this file anything you want. A `.tgz` extension is suggested, as the file is in *gzipped tar* archive format. All map contents are contained within the file, so a separate directory per map is not necessary.
15
+ Most commands need a map file to work on. Name this file anything you want. A `.tgz` extension is suggested, as the file is in *gzipped tar* archive format. All map contents are contained within the file.
27
16
 
28
17
  # Example
29
18
 
data/docs/add.md CHANGED
@@ -10,7 +10,7 @@ The forward-slash character is used indicate the nested folder structure of thes
10
10
 
11
11
  Some layers, such as vegetation layers, require a dataset to be present on your computer. Specify the location of the dataset with the `--path` option. The path can be absolute, or relative to the working directory.
12
12
 
13
- Raster layers (vegetation, shaded relief) typically have an appropriate image resolution set for that data. If desired, you can choose a different value using the `--resolution` option. Resolution is in metres per pixel, indicating the dataset quality rather than an output resolution such as pixels per inch.
13
+ Raster layers (vegetation, shaded relief) are typically imported at the data's native resolution. If desired, you can choose a different value using the `--resolution` option, for a data resolution in metres per pixel, or the `--ppi` option for output resolution in pixels per inch.
14
14
 
15
15
  For repeated use, it's easier to set the path or resolution for a layer in a permanent configuration file. Use the *config* command for this task.
16
16
 
data/docs/config.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Description
2
2
 
3
- Configure and view permanent *nswtopo* settings using the *config* command. For example, to set the *Google Chrome* path for rendering maps:
3
+ Configure and view permanent *nswtopo* settings using the *config* command. For example, to manually set the *Google Chrome* path:
4
4
 
5
5
  ```
6
6
  $ nswtopo config --chrome "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
data/docs/contours.md CHANGED
@@ -12,6 +12,8 @@ Choose a contour interval in metres using the `--interval` option. A five metre
12
12
 
13
13
  Noise in raw elevation data usually produces unsuitably rough contour lines. Some smoothing of the DEM removes most such artefacts. A default smoothing radius of 0.2mm is applied, configurable with the `--smooth` option. Increase the radius to produce smoother contours at the expense of detail.
14
14
 
15
+ DEM tiles are normally processed at their maximumum native resolution. Change this using the `--resolution` option. A reduced resolution (say 5 metres) can markedly improve processing speed for 1- and 2-metre tiles.
16
+
15
17
  # Layer Position
16
18
 
17
19
  Use an `--after`, `--before` or `--replace` option to insert the contours in an appropriate layer position. You will most likely want to replace an existing contour layer:
@@ -21,7 +23,7 @@ $ nswtopo contours --replace nsw.topographic.contours map.tgz DATA_25994.zip
21
23
  ```
22
24
  # Style
23
25
 
24
- Contours are rendered in brown at a thickness of 0.08mm. Change line colour with `--stroke`, thickness with `--stroke-width` and label colour with `--fill`. Colour can be an *RGB triplet* (e.g. *800080*) or *web colour* name (e.g. *purple*).
26
+ Contours are rendered in brown at a thickness of 0.08mm. Change line colour with `--stroke`, thickness with `--stroke-width` and label colour with `--fill`. Colour can be an *RGB triplet* (e.g. *#800080*) or *web colour* name (e.g. *purple*).
25
27
 
26
28
  # Contour Thinning
27
29
  A small contour interval can produce very dense contours in steep terrain. An advanced `--thin` option is available to selectively remove contours in steep areas such as cliffsides. It emulates a manual contour thinning technique. An index multiple of eight (e.g. 5m contours with 40m index contours) produces the most aesthetic results.
data/docs/init.md CHANGED
@@ -36,3 +36,11 @@ $ nswtopo init --dimensions 210,297 --coords 148.387,-36.148 map.tgz
36
36
  # Map Scale
37
37
 
38
38
  A 1:25000 scale is conventional and should work well for most purposes. Change to a smaller or larger scale using the `--scale` option.
39
+
40
+ # Map Inset
41
+
42
+ Use the `--inset` option to remove a rectangular inset from the map. Specify the inset location as two opposing corner coordinates in millimetres. For example, to remove a 50mm × 100mm inset from the map's top-left corner:
43
+
44
+ ```
45
+ $ nswtopo init --bounds bounds.kml --inset 0,0,50,100 map.tgz
46
+ ```