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
@@ -1,13 +1,48 @@
1
+ require_relative 'vector/cutout'
2
+ require_relative 'vector/knockout'
3
+
1
4
  module NSWTopo
2
5
  module Vector
3
- SVG_ATTRIBUTES = %w[fill-opacity fill font-family font-size font-style font-variant font-weight letter-spacing opacity stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width stroke text-decoration visibility word-spacing]
4
- FONT_SCALED_ATTRIBUTES = %w[word-spacing letter-spacing stroke-width line-height]
6
+ SVG_ATTRIBUTES = %w[
7
+ fill-opacity
8
+ fill
9
+ font-family
10
+ font-size
11
+ font-style
12
+ font-variant
13
+ font-weight
14
+ letter-spacing
15
+ opacity
16
+ paint-order
17
+ stroke-dasharray
18
+ stroke-dashoffset
19
+ stroke-linecap
20
+ stroke-linejoin
21
+ stroke-miterlimit
22
+ stroke-opacity
23
+ stroke-width
24
+ stroke
25
+ text-decoration
26
+ visibility
27
+ word-spacing
28
+ nswtopo:overprint
29
+ nswtopo:stroke
30
+ nswtopo:fill
31
+ ]
32
+
33
+ FONT_SCALED_ATTRIBUTES = %w[
34
+ word-spacing
35
+ letter-spacing
36
+ stroke-width
37
+ line-height
38
+ ]
39
+
5
40
  SHIELD_X, SHIELD_Y = 1.0, 0.5
6
41
  MARGIN = { mm: 1.0 }
7
42
  VALUE, POINT, ANGLE = "%.5f", "%.5f %.5f", "%.2f"
8
43
 
9
44
  def create
10
- @features = get_features.reproject_to(@map.projection).clip!(@map.bounding_box(MARGIN).coordinates.first)
45
+ @features = get_features.reproject_to(@map.neatline.projection).clip(@map.neatline(**MARGIN))
11
46
  @map.write filename, @features.to_json
12
47
  end
13
48
 
@@ -22,10 +57,6 @@ module NSWTopo
22
57
  extend Forwardable
23
58
  def_delegator :features, :none?, :empty?
24
59
 
25
- def to_mm
26
- @to_mm ||= @map.method(:coords_to_mm)
27
- end
28
-
29
60
  def drawing_features
30
61
  features.explode.reject do |feature|
31
62
  feature["draw"] == false
@@ -55,10 +86,10 @@ module NSWTopo
55
86
  distances = extras.segments.map(&:distance)
56
87
  offsets = midpoints.zip(distances).segments.map(&:transpose).map do |segment, distance|
57
88
  segment.along(distance.first / distance.inject(&:+))
58
- end.zip(points).map(&:difference)
59
- controls = midpoints.segments.zip(offsets).map do |segment, offset|
89
+ end.zip(points).map(&:diff)
90
+ controls = midpoints.segments.zip(offsets).flat_map do |segment, offset|
60
91
  segment.map { |point| [point, point.plus(offset)].along(fraction) }
61
- end.flatten(1).drop(1).each_slice(2).entries.prepend(nil)
92
+ end.drop(1).each_slice(2).entries.prepend(nil)
62
93
  points.zip(controls).map do |point, controls|
63
94
  controls ? "C %s %s %s" % [POINT, POINT, POINT] % [*controls.flatten, *point] : "M %s" % POINT % point
64
95
  end.join(" ")
@@ -74,51 +105,67 @@ module NSWTopo
74
105
  Array(key).any? do |selector|
75
106
  String(selector).split(?\s).to_set <= categories
76
107
  end
77
- end.values.inject(params, &:merge)
108
+ end.values.inject(params, &:deep_merge)
78
109
  end
79
110
 
80
- def render(group, defs)
111
+ def render(knockout:, **, &block)
112
+ defs = REXML::Element.new("defs").tap(&block)
113
+ defs.add_attributes "id" => "#{@name}.defs"
114
+
81
115
  drawing_features.group_by do |feature, categories|
82
116
  categories || Array(feature["category"]).map(&:to_s).map(&method(:categorise)).to_set
83
- end.map do |categories, features|
117
+ end.flat_map do |categories, features|
84
118
  dupes = params_for(categories)["dupe"]
85
119
  Array(dupes).map(&:to_s).map do |dupe|
86
120
  [categories | Set[dupe], [name, *categories, "content"].join(?.)]
87
121
  end.push [categories, features]
88
- end.flatten(1).map do |categories, features|
122
+ end.tap do |ordered|
123
+ params.fetch("order", []).reverse.map(&:split).map(&:to_set).each do |filter|
124
+ ordered.sort_by!.with_index do |(categories, features), index|
125
+ [filter <= categories ? 0 : 1, index]
126
+ end
127
+ end
128
+ end.each do |categories, features|
89
129
  ids = [name, *categories]
130
+ use = REXML::Element.new("use")
131
+ use.add_attributes "id" => ids.join(?.)
132
+
90
133
  case features
91
134
  when String
92
- container = group.add_element "use", "class" => categories.to_a.join(?\s), "xlink:href" => "#%s" % features
135
+ use.add_attributes "href" => "##{features}"
93
136
  when Array
94
- container = group.add_element "g", "class" => categories.to_a.join(?\s)
95
- content = container.add_element "g", "id" => [*ids, "content"].join(?.)
137
+ content = defs.add_element "g", "id" => [*ids, "content"].join(?.)
138
+ use.add_attributes "href" => "#" + [*ids, "content"].join(?.)
96
139
  end
97
- container.add_attribute "id", ids.join(?.) if categories.any?
140
+ use.tap(&block)
98
141
 
99
- commands = params_for categories
100
- font_size, bezier, section = commands.values_at "font-size", "bezier", "section"
101
- commands.slice(*FONT_SCALED_ATTRIBUTES).each do |key, value|
102
- commands[key] = commands[key].to_i * font_size * 0.01 if value =~ /^\d+%$/
103
- end if font_size
142
+ category_params = params_for(categories)
143
+ font_size, stroke_width, bezier, section = category_params.values_at "font-size", "stroke-width", "bezier", "section"
144
+
145
+ category_params.slice(*SVG_ATTRIBUTES).tap do |svg_attributes|
146
+ svg_attributes.slice(*FONT_SCALED_ATTRIBUTES).each do |key, value|
147
+ svg_attributes[key] = svg_attributes[key].to_i * font_size * 0.01 if /^\d+%$/ === value
148
+ end if font_size
149
+ use.add_attributes svg_attributes
150
+ end
104
151
 
105
152
  features.each do |feature, _|
106
153
  case feature
107
154
  when GeoJSON::Point
108
155
  symbol_id = [*ids, "symbol"].join(?.)
109
- transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*feature.coordinates.yield_self(&to_mm), feature.fetch("rotation", @map.rotation) - @map.rotation]
110
- content.add_element "use", "transform" => transform, "xlink:href" => "#%s" % symbol_id
156
+ transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*feature.coordinates, feature.fetch("rotation", @map.rotation) - @map.rotation]
157
+ content.add_element "use", "transform" => transform, "href" => "#%s" % symbol_id
111
158
 
112
159
  when GeoJSON::LineString
113
- linestring = feature.coordinates.map(&to_mm)
160
+ linestring = feature.coordinates
114
161
  (section ? linestring.in_sections(section) : [linestring]).each do |linestring|
115
162
  content.add_element "path", "fill" => "none", "d" => svg_path_data(linestring, bezier: bezier)
116
163
  end
117
164
 
118
165
  when GeoJSON::Polygon
119
166
  path_data = feature.coordinates.map do |ring|
120
- svg_path_data ring.map(&to_mm), bezier: bezier
121
- end.join(" Z ").concat(" Z")
167
+ svg_path_data ring, bezier: bezier
168
+ end.each.with_object("Z").entries.join(?\s)
122
169
  content.add_element "path", "fill-rule" => "nonzero", "d" => path_data
123
170
 
124
171
  when REXML::Element
@@ -132,28 +179,25 @@ module NSWTopo
132
179
  end
133
180
  end if content
134
181
 
135
- commands.each do |command, args|
182
+ category_params.each do |command, args|
136
183
  next unless args
137
184
  args = args.map(&:to_a).inject([], &:+) if Array === args && args.all?(Hash)
138
185
 
139
186
  case command
140
187
  when "blur"
141
188
  filter_id = [*ids, "blur"].join(?.)
142
- container.add_attribute "filter", "url(#%s)" % filter_id
189
+ use.add_attribute "filter", "url(#%s)" % filter_id
143
190
  defs.add_element("filter", "id" => filter_id).add_element "feGaussianBlur", "stdDeviation" => args, "in" => "SourceGraphic"
144
191
 
145
- when "opacity"
146
- if categories.none?
147
- group.add_attribute "style", "opacity:#{args}"
148
- else
149
- container.add_attribute "opacity", args
150
- end
151
-
152
192
  when "symbol"
153
193
  next unless content
154
194
  symbol = defs.add_element "g", "id" => [*ids, "symbol"].join(?.)
155
195
  args.each do |element, attributes|
156
- symbol.add_element element, attributes
196
+ if attributes
197
+ symbol.add_element element, attributes
198
+ else
199
+ symbol.add_element REXML::Document.new(element).root
200
+ end
157
201
  end
158
202
 
159
203
  when "pattern"
@@ -166,25 +210,25 @@ module NSWTopo
166
210
  args.each do |element, attributes|
167
211
  pattern.add_element element, attributes
168
212
  end
169
- container.add_attribute "fill", "url(#%s)" % pattern_id
213
+ use.add_attribute "fill", "url(#%s)" % pattern_id
170
214
 
171
215
  when "symbolise"
172
216
  next unless content
173
- interval, symbols = args.partition do |element, attributes|
174
- element == "interval"
217
+ args, symbols = args.partition do |element, attributes|
218
+ %w[interval offset].include? element
175
219
  end
176
- interval = Hash[interval]["interval"]
220
+ interval, offset = Hash[args].values_at "interval", "offset"
177
221
  symbol_ids = symbols.map.with_index do |(element, attributes), index|
178
222
  symbol_id = [*ids, "symbol", index].join(?.).tap do |symbol_id|
179
223
  defs.add_element("g", "id" => symbol_id).add_element(element, attributes)
180
224
  end
181
225
  end
182
226
  lines_or_rings = features.grep(GeoJSON::LineString).map(&:coordinates)
183
- lines_or_rings += features.grep(GeoJSON::Polygon).map(&:coordinates).flatten(1)
227
+ lines_or_rings += features.grep(GeoJSON::Polygon).flat_map(&:coordinates)
184
228
  lines_or_rings.each do |points|
185
- points.map(&to_mm).sample_at(interval, angle: true).each do |point, angle|
229
+ points.sample_at(interval, angle: true, offset: offset).each do |point, angle|
186
230
  transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*point, 180.0 * angle / Math::PI]
187
- content.add_element "use", "transform" => transform, "xlink:href" => "#%s" % symbol_ids.sample
231
+ content.add_element "use", "transform" => transform, "href" => "#%s" % symbol_ids.sample
188
232
  end
189
233
  end
190
234
 
@@ -195,66 +239,54 @@ module NSWTopo
195
239
  args.each do |element, attributes|
196
240
  symbol.add_element element, attributes
197
241
  end
198
- features.grep(GeoJSON::LineString).map do |feature|
199
- feature.coordinates.map(&to_mm)
200
- end.each do |line|
242
+ features.grep(GeoJSON::LineString).map(&:coordinates).each do |line|
201
243
  case command
202
244
  when "inpoint" then [line.first(2)]
203
245
  when "outpoint" then [line.last(2).rotate]
204
246
  when "endpoint" then [line.first(2), line.last(2).rotate]
205
247
  end.each do |segment|
206
- transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*segment.first, 180.0 * segment.difference.angle / Math::PI]
207
- container.add_element "use", "transform" => transform, "xlink:href" => "#%s" % symbol_id
248
+ transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*segment.first, 180.0 * segment.diff.angle / Math::PI]
249
+ use.add_element "use", "transform" => transform, "href" => "#%s" % symbol_id
208
250
  end
209
251
  end
210
252
 
211
- when "mask"
212
- next unless args && content && content.elements.any?
213
- filter_id, mask_id = %w[raster-mask.filter raster-mask]
214
- mask_contents = defs.elements["mask[@id='%s']/g[@filter]" % mask_id]
215
- mask_contents ||= begin
216
- defs.add_element("filter", "id" => filter_id).add_element "feColorMatrix", "type" => "matrix", "in" => "SourceGraphic", "values" => "0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 -1 1"
217
- defs.add_element("mask", "id" => mask_id).add_element("g", "filter" => "url(#%s)" % filter_id).tap do |mask_contents|
218
- mask_contents.add_element "rect", "width" => "100%", "height" => "100%", "fill" => "none", "stroke" => "none"
219
- end
220
- end
221
- transforms = REXML::XPath.each(content, "ancestor::g[@transform]/@transform").map(&:value)
222
- mask_contents.add_element "use", "xlink:href" => "#%s" % content.attributes["id"], "transform" => (transforms.join(?\s) if transforms.any?)
253
+ when "knockout"
254
+ use.add_attributes "mask" => "url(##{knockout})"
255
+ Knockout.new(use, *args).tap(&block)
256
+
257
+ when "preserve"
258
+ use.add_attributes "mask" => "none"
259
+
260
+ when "cutout", "mask" # mask deprecated
261
+ Cutout.new(use).tap(&block)
223
262
 
224
- when "fence"
263
+ when "barrier", "fence" # fence deprecated
225
264
  next unless content && args
226
- buffer = 0.5 * (Numeric === args ? args : commands.fetch("stroke-width", 0))
227
- features.each do |feature|
228
- next if REXML::Element === feature
229
- yield feature, buffer
265
+ buffer = 0.5 * (Numeric === args ? args : Numeric === stroke_width ? stroke_width : 0)
266
+ features.grep_v(REXML::Element).each do |feature|
267
+ Labels::Barrier.new(feature, buffer).tap(&block)
230
268
  end
231
269
 
232
270
  when "shield"
233
271
  next unless content
234
272
  content.elements.each("text") do |element|
235
- next unless text_length = element.elements["./ancestor-or-self::[@textLength]/@textLength"]&.value&.to_f
236
- shield = REXML::Element.new("g")
237
- width, height = text_length + SHIELD_X * font_size, (1 + SHIELD_Y) * font_size
238
- shield.add_element "rect", "x" => -0.5 * width, "y" => -0.5 * height, "width" => width, "height" => height, "rx" => font_size * 0.3, "ry" => font_size * 0.3, "stroke" => "none", "fill" => args
239
- text_transform = element.attributes.get_attribute "transform"
240
- text_transform.remove
241
- shield.attributes << text_transform
242
- element.parent.elements << shield
243
- shield << element
273
+ case
274
+ when text_length = element.elements["./ancestor-or-self::[@textLength]/@textLength"]&.value&.to_f
275
+ shield = REXML::Element.new("g")
276
+ width, height = text_length + SHIELD_X * font_size, (1 + SHIELD_Y) * font_size
277
+ shield.add_element "rect", "x" => -0.5 * width, "y" => -0.5 * height, "width" => width, "height" => height, "rx" => font_size * 0.3, "ry" => font_size * 0.3, "stroke" => "none", "fill" => args
278
+ text_transform = element.attributes.get_attribute "transform"
279
+ text_transform.remove
280
+ shield.attributes << text_transform
281
+ element.parent.elements << shield
282
+ shield << element
283
+ when href = element.elements["./textPath[@href]/@href"]&.value
284
+ shield = REXML::Element.new("g")
285
+ shield.add_element "use", "href" => href, "stroke-width" => (1 + SHIELD_Y) * font_size, "stroke" => args, "stroke-linecap" => "round"
286
+ element.parent.elements << shield
287
+ shield << element
288
+ end
244
289
  end
245
-
246
- when *SVG_ATTRIBUTES
247
- container.add_attribute command, args
248
- end
249
- end
250
-
251
- next categories, features, container
252
- end.tap do |categorised|
253
- params.fetch("order", []).reverse.map(&:split).map(&:to_set).each do |filter|
254
- categorised.select do |categories, features, container|
255
- filter <= categories
256
- end.reverse.each do |categories, features, container|
257
- group.unshift container.remove
258
290
  end
259
291
  end
260
292
  end
@@ -1,17 +1,17 @@
1
1
  module NSWTopo
2
2
  module Vegetation
3
- include Raster, GDALGlob
4
- CREATE = %w[mapping contrast colour]
3
+ include Raster, MaskRender, GDALGlob
4
+ CREATE = %w[mapping contrast]
5
+ DEFAULTS = YAML.load <<~YAML
6
+ colour: hsl(75,55%,72%)
7
+ YAML
5
8
 
6
9
  def get_raster(temp_dir)
7
- txt_path = temp_dir / "source.txt"
8
- vrt_path = temp_dir / "source.vrt"
9
-
10
+ @params["colour"] = @params["colour"]["woody"] if Hash === @params["colour"]
10
11
  min, max = minmax = @mapping&.values_at("min", "max")
11
- low, high, factor = { "low" => 0, "high" => 100, "factor" => 0.0 }.merge(@contrast || {}).values_at "low", "high", "factor"
12
- woody, nonwoody = { "woody" => "#A6F1A6", "non-woody" => "#FFFFFF" }.merge(@colour || {}).values_at("woody", "non-woody").map { |string| Colour.new string }
12
+ low, high, factor = [0, 100, 0].zip(Array @contrast&.values_at("low", "high", "factor")).map(&:compact).map(&:last)
13
13
 
14
- colour_table = (0..255).map do |index|
14
+ alpha_table = (0..255).map do |index|
15
15
  case
16
16
  when minmax&.all?(Integer) && minmax.all?(0..255)
17
17
  (100.0 * (index - min) / (max - min)).clamp(0.0, 100.0)
@@ -29,45 +29,50 @@ module NSWTopo
29
29
  end.inject(&:-)
30
30
  end.inject(&:/) # sigmoid between 0..1
31
31
  end.map do |x|
32
- nonwoody.mix(woody, x)
32
+ Integer(255 * x)
33
33
  end
34
34
 
35
35
  Dir.chdir(@source ? @source.parent : Pathname.pwd) do
36
36
  gdal_rasters @path
37
- end.tap do |rasters|
38
- raise "no vegetation data file specified" if rasters.none?
37
+ end.each do |path, info|
38
+ raise "can't process vegetation data for #{@name}" unless info["bands"].one?
39
+ raise "can't process vegetation data for #{@name}" unless info.dig("bands", 0, "colorInterpretation") == "Palette"
40
+ raise "can't process vegetation data for #{@name}" unless info.dig("bands", 0, "colorTable", "count") == 256
41
+ end.group_by do |path, info|
42
+ info.dig("bands", 0).values_at("colorTable", "noDataValue")
43
+ end.values.then do |rasters, *others|
44
+ raise "no vegetation data file specified" unless rasters
45
+ raise "can't process vegetation data for #{@name}" if others.any?
46
+ rasters
39
47
  end.group_by do |path, info|
40
48
  Projection.new info.dig("coordinateSystem", "wkt")
41
49
  end.map.with_index do |(projection, rasters), index|
42
- indexed_tif_path = temp_dir / "indexed.#{index}.tif"
43
- indexed_vrt_path = temp_dir / "indexed.#{index}.vrt"
44
- coloured_tif_path = temp_dir / "coloured.#{index}.tif"
45
- tif_path = temp_dir / "output.#{index}.tif"
50
+ vrt_path = temp_dir / "indexed.#{index}.vrt"
51
+ txt_path = temp_dir / "source.txt"
46
52
 
47
53
  txt_path.write rasters.map(&:first).join(?\n)
48
- OS.gdalbuildvrt "-overwrite", "-input_file_list", txt_path, vrt_path
49
- OS.gdal_translate "-projwin", *@map.projwin(projection), "-r", "near", "-co", "TFW=YES", vrt_path, indexed_tif_path
50
- OS.gdal_translate "-of", "VRT", indexed_tif_path, indexed_vrt_path
54
+ OS.gdalbuildvrt "-overwrite", "-r", "nearest", "-input_file_list", txt_path, vrt_path
51
55
 
52
- xml = REXML::Document.new indexed_vrt_path.read
53
- raise "can't process vegetation data for #{@name}" unless xml.elements.each("/VRTDataset/VRTRasterBand/ColorTable", &:itself).one?
54
- raise "can't process vegetation data for #{@name}" unless xml.elements.each("/VRTDataset/VRTRasterBand/ColorTable/Entry", &:itself).count == 256
55
- xml.elements.collect("/VRTDataset/VRTRasterBand/ColorTable/Entry", &:itself).zip(colour_table) do |entry, colour|
56
- entry.attributes["c1"], entry.attributes["c2"], entry.attributes["c3"], entry.attributes["c4"] = *colour.triplet, 255
56
+ xml = REXML::Document.new vrt_path.read
57
+ xml.elements.collect("/VRTDataset/VRTRasterBand/ColorTable/Entry", &:itself).zip(alpha_table) do |entry, alpha|
58
+ entry.attributes["c1"], entry.attributes["c2"], entry.attributes["c3"], entry.attributes["c4"] = alpha, alpha, alpha, 255
57
59
  end
58
- xml.elements.each("/VRTDataset/VRTRasterBand/NoDataValue", &:remove)
59
- indexed_vrt_path.write xml
60
- OS.gdal_translate "-expand", "rgb", indexed_vrt_path, coloured_tif_path
61
60
 
62
- OS.gdalwarp "-s_srs", projection, "-t_srs", @map.projection, "-r", "bilinear", coloured_tif_path, tif_path
63
- next tif_path, Numeric === @resolution ? @resolution : @map.get_raster_resolution(tif_path)
64
- end.transpose.tap do |tif_paths, resolutions|
65
- @resolution = resolutions.min
66
- txt_path.write tif_paths.join(?\n)
67
- OS.gdalbuildvrt "-overwrite", "-input_file_list", txt_path, vrt_path
68
- end
61
+ vrt_path.write xml
62
+ vrt_path
63
+ end.then do |vrt_paths|
64
+ tif_path = temp_dir / "source.tif"
65
+ vrt_path = temp_dir / "source.vrt"
69
66
 
70
- return @resolution, vrt_path
67
+ args = ["-t_srs", @map.projection, "-r", "nearest", "-cutline", "GeoJSON:/vsistdin/", "-crop_to_cutline"]
68
+ args += ["-tr", @mm_per_px, @mm_per_px] if @mm_per_px
69
+ OS.gdalwarp *args, *vrt_paths, tif_path do |stdin|
70
+ stdin.puts @map.cutline.to_json
71
+ end
72
+ OS.gdal_translate "-expand", "gray", "-a_nodata", "none", tif_path, vrt_path
73
+
74
+ return vrt_path
75
+ end
71
76
  end
72
77
  end
73
78
  end
data/lib/nswtopo/layer.rb CHANGED
@@ -1,8 +1,12 @@
1
+ require_relative 'layer/raster_import'
1
2
  require_relative 'layer/raster'
3
+ require_relative 'layer/raster_render'
4
+ require_relative 'layer/mask_render'
2
5
  require_relative 'layer/vector'
3
6
  require_relative 'layer/vegetation'
4
7
  require_relative 'layer/import'
5
8
  require_relative 'layer/arcgis_raster'
9
+ require_relative 'layer/colour_mask'
6
10
  require_relative 'layer/feature'
7
11
  require_relative 'layer/contour'
8
12
  require_relative 'layer/spot'
@@ -15,19 +19,28 @@ require_relative 'layer/labels'
15
19
 
16
20
  module NSWTopo
17
21
  class Layer
18
- TYPES = Set[Vegetation, Import, ArcGISRaster, Feature, Contour, Spot, Overlay, Relief, Grid, Declination, Control, Labels]
22
+ TYPES = Set[Vegetation, Import, ColourMask, ArcGISRaster, Feature, Contour, Spot, Overlay, Relief, Grid, Declination, Control, Labels]
19
23
 
20
24
  def initialize(name, map, params)
25
+ params.delete("min-version").then do |creator_string|
26
+ creator_string ? Version[creator_string] : VERSION
27
+ rescue Version::Error
28
+ raise "layer '%s' has unrecognised version: %s" % [name, creator_string]
29
+ end.then do |min_version|
30
+ raise "layer '%s' requires nswtopo %s, this version: %s" % [name, min_version, VERSION] unless min_version <= VERSION
31
+ end
32
+
21
33
  @type = begin
22
34
  NSWTopo.const_get params["type"]
23
35
  rescue NameError, TypeError
24
36
  end
25
37
 
26
- raise "unrecognised layer type: %s" % params["type"].inspect unless TYPES === @type
38
+ raise "layer '%s' has unrecognised type: %s" % [name, params["type"].inspect] unless TYPES === @type
27
39
  extend @type
28
40
 
29
- @params = @type.const_defined?(:DEFAULTS) ? @type.const_get(:DEFAULTS).transform_keys(&:to_s).merge(params) : params
30
- @name, @map, @source, @path, @resolution = Layer.sanitise(name), map, @params.delete("source"), @params.delete("path"), @params.delete("resolution")
41
+ @params = @type.const_defined?(:DEFAULTS) ? @type.const_get(:DEFAULTS).transform_keys(&:to_s).deep_merge(params) : params
42
+ @name, @map, @source, @path, resolution, ppi = Layer.sanitise(name), map, @params.delete("source"), @params.delete("path"), @params.delete("resolution"), @params.delete("ppi")
43
+ @mm_per_px = ppi ? 25.4 / ppi : resolution ? @map.to_mm(resolution) : nil
31
44
 
32
45
  @type.const_get(:CREATE).map(&:to_s).each do |attr|
33
46
  instance_variable_set ?@ + attr.tr_s(?-, ?_), @params.delete(attr)
@@ -39,23 +52,24 @@ module NSWTopo
39
52
 
40
53
  def level
41
54
  case
42
- when Vegetation == @type then 0
43
- when Import == @type then 1
44
- when ArcGISRaster == @type then 1
45
- when Feature == @type then 2
46
- when Contour == @type then 2
47
- when Spot == @type then 2
48
- when Overlay == @type then 3
49
- when Relief == @type then 4
50
- when Grid == @type then 5
51
- when Declination == @type then 6
52
- when Control == @type then 7
55
+ when Import == @type then 0
56
+ when ArcGISRaster == @type then 0
57
+ when Vegetation == @type then 1
58
+ when ColourMask == @type then 2
59
+ when Feature == @type then 3
60
+ when Contour == @type then 3
61
+ when Spot == @type then 3
62
+ when Overlay == @type then 4
63
+ when Relief == @type then 5
64
+ when Grid == @type then 6
65
+ when Declination == @type then 7
66
+ when Control == @type then 8
53
67
  when Labels == @type then 99
54
68
  end
55
69
  end
56
70
 
57
71
  def <=>(other)
58
- [self, other].map(&:level).inject(&:<=>)
72
+ self.level <=> other.level
59
73
  end
60
74
 
61
75
  def ==(other)