nswtopo 3.0 → 3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/bin/nswtopo +19 -4
  3. data/docs/contours.md +2 -0
  4. data/docs/relief.md +2 -3
  5. data/docs/spot-heights.md +2 -0
  6. data/lib/nswtopo/archive.rb +6 -3
  7. data/lib/nswtopo/chrome.rb +16 -8
  8. data/lib/nswtopo/commands/layers.rb +2 -2
  9. data/lib/nswtopo/config.rb +1 -0
  10. data/lib/nswtopo/formats/gemf.rb +1 -0
  11. data/lib/nswtopo/formats/kmz.rb +16 -10
  12. data/lib/nswtopo/formats/mbtiles.rb +1 -0
  13. data/lib/nswtopo/formats/pdf.rb +34 -4
  14. data/lib/nswtopo/formats/svg.rb +4 -4
  15. data/lib/nswtopo/formats/svgz.rb +1 -0
  16. data/lib/nswtopo/formats/zip.rb +5 -4
  17. data/lib/nswtopo/formats.rb +35 -36
  18. data/lib/nswtopo/geometry/r_tree.rb +24 -23
  19. data/lib/nswtopo/geometry/straight_skeleton/node.rb +4 -4
  20. data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +51 -40
  21. data/lib/nswtopo/geometry/straight_skeleton/split.rb +2 -2
  22. data/lib/nswtopo/geometry/vector.rb +55 -49
  23. data/lib/nswtopo/geometry.rb +0 -5
  24. data/lib/nswtopo/gis/arcgis/layer/map.rb +11 -10
  25. data/lib/nswtopo/gis/arcgis/layer/query.rb +8 -10
  26. data/lib/nswtopo/gis/arcgis/layer.rb +7 -11
  27. data/lib/nswtopo/gis/dem.rb +3 -2
  28. data/lib/nswtopo/gis/gdal_glob.rb +3 -3
  29. data/lib/nswtopo/gis/geojson/collection.rb +59 -13
  30. data/lib/nswtopo/gis/geojson/line_string.rb +142 -1
  31. data/lib/nswtopo/gis/geojson/multi_line_string.rb +49 -7
  32. data/lib/nswtopo/gis/geojson/multi_point.rb +87 -0
  33. data/lib/nswtopo/gis/geojson/multi_polygon.rb +35 -23
  34. data/lib/nswtopo/gis/geojson/point.rb +16 -1
  35. data/lib/nswtopo/gis/geojson/polygon.rb +69 -7
  36. data/lib/nswtopo/gis/geojson.rb +92 -46
  37. data/lib/nswtopo/gis/projection.rb +5 -1
  38. data/lib/nswtopo/helpers/thread_pool.rb +39 -0
  39. data/lib/nswtopo/helpers.rb +44 -5
  40. data/lib/nswtopo/layer/arcgis_raster.rb +3 -3
  41. data/lib/nswtopo/layer/contour.rb +24 -26
  42. data/lib/nswtopo/layer/control.rb +5 -3
  43. data/lib/nswtopo/layer/declination.rb +14 -10
  44. data/lib/nswtopo/layer/feature.rb +5 -5
  45. data/lib/nswtopo/layer/grid.rb +19 -18
  46. data/lib/nswtopo/layer/labels/barriers.rb +23 -0
  47. data/lib/nswtopo/layer/labels/convex_hull.rb +12 -0
  48. data/lib/nswtopo/layer/labels/convex_hulls.rb +86 -0
  49. data/lib/nswtopo/layer/labels/label.rb +63 -0
  50. data/lib/nswtopo/layer/labels.rb +192 -315
  51. data/lib/nswtopo/layer/overlay.rb +11 -12
  52. data/lib/nswtopo/layer/raster.rb +1 -0
  53. data/lib/nswtopo/layer/relief.rb +6 -4
  54. data/lib/nswtopo/layer/spot.rb +11 -17
  55. data/lib/nswtopo/layer/{vector → vector_render}/cutout.rb +1 -1
  56. data/lib/nswtopo/layer/{vector → vector_render}/knockout.rb +2 -3
  57. data/lib/nswtopo/layer/{vector.rb → vector_render.rb} +20 -45
  58. data/lib/nswtopo/layer.rb +2 -1
  59. data/lib/nswtopo/map.rb +62 -60
  60. data/lib/nswtopo/svg.rb +5 -0
  61. data/lib/nswtopo/tiled_web_map.rb +3 -3
  62. data/lib/nswtopo/tree_indenter.rb +2 -2
  63. data/lib/nswtopo/version.rb +1 -1
  64. data/lib/nswtopo.rb +8 -0
  65. metadata +15 -17
  66. data/lib/nswtopo/geometry/overlap.rb +0 -47
  67. data/lib/nswtopo/geometry/segment.rb +0 -27
  68. data/lib/nswtopo/geometry/vector_sequence.rb +0 -180
  69. data/lib/nswtopo/helpers/array.rb +0 -19
  70. data/lib/nswtopo/helpers/concurrently.rb +0 -27
  71. data/lib/nswtopo/helpers/dir.rb +0 -7
  72. data/lib/nswtopo/helpers/hash.rb +0 -15
  73. data/lib/nswtopo/helpers/tar_writer.rb +0 -11
  74. data/lib/nswtopo/layer/labels/barrier.rb +0 -39
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 505b2d8c4c44965fcad1403d1c2a266f7ce22f64225e97d6be2e7b2057ad2791
4
- data.tar.gz: 9ba473cddff584981acaee0efe47b8fe8095681792da886f08a3979e8f9807b4
3
+ metadata.gz: c90485c22ed73dd281bbb6abb7dafd9ec919edda7065dd7106b0acd12485acf3
4
+ data.tar.gz: fc3bae27f7d28243a3e9e27e56c28c8c0a724ea92237c018e4b161c9ad1ee74a
5
5
  SHA512:
6
- metadata.gz: dfccb5d8112650a894378967d6d67fe42b5a303ce3140bfd6ced96ca74472f96df2303fe15d69b3da710277e62eba38e8307be374b4cef7e730253fce6c10f1e
7
- data.tar.gz: b707d3f3a619f7001ea2f3a5dbcf8f0ac603ce080f49cd44b614b385bab8a080f922c629172cefb63b13ab1e27e79bb538a887abb88a56ea2648cb8950a69a10
6
+ metadata.gz: 70779d0482c7d5350f65af71faa1503f3f290125e38bbe16e9017c10332da375e763ac4583907f43e741b612f928563379c32b7ec38652b22ffe2d2d34827519
7
+ data.tar.gz: d67a0b8e445cfec80c7ccae5d3df98983ec82b5533188e07e232f8dc41f825d70cc98026b5bb566127256eb22d4721e0aa1f697cccd80f98a85ee6cea8fdb006
data/bin/nswtopo CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Copyright 2011-2022 Matthew Hollingworth
3
+ # Copyright 2011-2023 Matthew Hollingworth
4
4
  #
5
5
  # This program is free software: you can redistribute it and/or modify
6
6
  # it under the terms of the GNU Affero General Public License as published by
@@ -37,8 +37,8 @@ begin
37
37
  end
38
38
 
39
39
  case
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"
40
+ when (RUBY_VERSION.split(/\D+/).take(3).map(&:to_i) <=> [3,1,4]) < 0
41
+ log_abort "ruby 3.1.4 or greater required"
42
42
  when !Zlib.const_defined?(:GzipFile)
43
43
  log_abort "ruby with GZIP_SUPPORT required"
44
44
  when (GDAL_VERSION.split(/\D+/).take(3).map(&:to_i) <=> [3,4]) < 0
@@ -56,6 +56,7 @@ begin
56
56
  NonNegFloat = Class.new
57
57
  Dimensions = Class.new
58
58
  Inset = Class.new
59
+ Radius = Class.new
59
60
  Margins = Class.new
60
61
  CoordList = Class.new
61
62
  Rotation = Class.new
@@ -90,6 +91,13 @@ begin
90
91
  string.split(?,).map(&:to_f).each_slice(4).entries
91
92
  end
92
93
 
94
+ OptionParser.accept Radius, /\A#{float}(?:,#{digits})*\z/ do |string|
95
+ radius, segments = string.split(?,)
96
+ raise OptionParser::InvalidArgument, string unless radius.to_f.positive?
97
+ raise OptionParser::InvalidArgument, string if segments && segments.to_i.zero?
98
+ segments ? [radius.to_f, segments.to_i] : radius.to_f
99
+ end
100
+
93
101
  OptionParser.accept Margins, /\A#{float}(?:,#{float})?\z/ do |string|
94
102
  margins = string.split(?,).map(&:to_f)
95
103
  raise OptionParser::InvalidArgument, string if margins.any?(&:negative?)
@@ -111,7 +119,7 @@ begin
111
119
  end
112
120
 
113
121
  OptionParser.accept Colour do |string|
114
- string == "none" ? string : Colour.new(string.downcase)
122
+ string == "none" ? string : Colour.new(string.downcase).to_s
115
123
  rescue Colour::Error
116
124
  raise OptionParser::InvalidArgument, string
117
125
  end
@@ -217,6 +225,7 @@ begin
217
225
  "degrees, 'auto' or 'magnetic'"
218
226
  parser.on "-m", "--margins <x[,y]>", Margins, "map margins in mm"
219
227
  parser.on "-i", "--inset <x1,y1,x2,y2>", Inset, "map inset coordinates in mm"
228
+ parser.on "--radius <radius>", Radius, "map corner radius in mm"
220
229
  parser.on "-o", "--overwrite", "overwrite existing map file"
221
230
 
222
231
  when "info"
@@ -265,6 +274,7 @@ begin
265
274
  parser.on "--stroke-width <width>", PositiveFloat, "stroke width in mm (default %s)" % NSWTopo::Contour::DEFAULTS["stroke-width"]
266
275
  parser.on "--fill <colour>", Colour, "label colour (defaults to stroke colour)"
267
276
  parser.on "-r", "--resolution <resolution>", PositiveFloat, "DEM processing resolution in metres"
277
+ parser.on "--epsg <epsg>", PositiveInt, "override EPSG projection code for DEM"
268
278
 
269
279
  when "spot-heights"
270
280
  parser.banner = <<~EOF
@@ -279,6 +289,7 @@ begin
279
289
  parser.on "-b", "--before <layer>", "insert before specified layer"
280
290
  parser.on "-c", "--replace <layer>", "replace specified layer"
281
291
  parser.on "-r", "--resolution <resolution>", PositiveFloat, "DEM processing resolution in metres"
292
+ parser.on "--epsg <epsg>", PositiveInt, "override EPSG projection code for DEM"
282
293
 
283
294
  when "relief"
284
295
  parser.banner = <<~EOF
@@ -292,6 +303,7 @@ begin
292
303
  parser.on "-m", "--method <igor|combined>", %w[igor combined], "relief shading method (default %s)" % NSWTopo::Relief::DEFAULTS["method"]
293
304
  parser.on "-z", "--azimuth <azimuth>", Float, "azimuth in degrees (default %i)" % NSWTopo::Relief::DEFAULTS["azimuth"]
294
305
  parser.on "-f", "--factor <factor>", PositiveFloat, "exaggeration factor (default %s)" % NSWTopo::Relief::DEFAULTS["factor"]
306
+ parser.on "--epsg <epsg>", PositiveInt, "override EPSG projection code for DEM"
295
307
 
296
308
  when "grid"
297
309
  parser.banner = <<~EOF
@@ -538,4 +550,7 @@ rescue Interrupt
538
550
  log_abort "interrupted"
539
551
  rescue RuntimeError => error
540
552
  log_abort error.message
553
+ rescue StandardError
554
+ print "\r\e[2K" if $stdout.tty?
555
+ raise
541
556
  end
data/docs/contours.md CHANGED
@@ -14,6 +14,8 @@ Noise in raw elevation data usually produces unsuitably rough contour lines. Som
14
14
 
15
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
16
 
17
+ Use the `--epsg` option to override the EPSG projection code for the DEM, in case of missing metadata (e.g. ACT 2015 DEM tiles).
18
+
17
19
  # Layer Position
18
20
 
19
21
  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:
data/docs/relief.md CHANGED
@@ -14,9 +14,8 @@ No configuration is needed to get good results from ELVIS data. Use the followin
14
14
  * **resolution**: resolution for the DEM data; a lower value will reduce file size but yields a smoother effect
15
15
  * **opacity**: overall layer opacity
16
16
  * **altitude**: raking angle of the light from the horizon
17
- * **azimuth**: azimuth angle of the light, clockwise from north; deviation from the 315° default can be counter-intuitive
18
- * **sources**: number of light sources to use for multi-directional shading
19
- * **yellow**: amount of yellow illumination to apply as a fraction of grey shading
20
17
  * **factor**: vertical exaggeration factor
21
18
 
22
19
  Opacity and exaggeration can both be used to adjust the subtly of the shading effect.
20
+
21
+ Use the `--epsg` option to override the EPSG projection code for the DEM, in case of missing metadata (e.g. ACT 2015 DEM tiles).
data/docs/spot-heights.md CHANGED
@@ -18,6 +18,8 @@ Use the `--extent` option to set a minimum size in millimetres when searching fo
18
18
 
19
19
  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.
20
20
 
21
+ Use the `--epsg` option to override the EPSG projection code for the DEM, in case of missing metadata (e.g. ACT 2015 DEM tiles).
22
+
21
23
  # Layer Position
22
24
 
23
25
  Use an `--after`, `--before` or `--replace` option to insert the spot heights in an appropriate layer position. You will most likely want to replace an existing spot heights layer:
@@ -1,5 +1,6 @@
1
1
  module NSWTopo
2
2
  class Archive
3
+ using Helpers
3
4
  extend Safely
4
5
  include Enumerable
5
6
  Invalid = Class.new RuntimeError
@@ -20,7 +21,7 @@ module NSWTopo
20
21
  yield entry unless @entries.key? entry.full_name
21
22
  end
22
23
  @entries.each do |filename, entry|
23
- yield entry if entry
24
+ yield entry.tap(&:rewind) if entry
24
25
  end
25
26
  end
26
27
 
@@ -71,8 +72,10 @@ module NSWTopo
71
72
  end if in_path && false != Config["versioning"]
72
73
  Gem::Package::TarReader.new(input) do |tar|
73
74
  archive = new(tar).tap(&block)
74
- Gem::Package::TarWriter.new(buffer) do |tar|
75
- archive.each &tar.method(:add_entry)
75
+ archive.each do |entry|
76
+ buffer.write entry.header
77
+ buffer.write entry.read
78
+ buffer.write ?\0 while buffer.pos % 512 > 0
76
79
  end if archive.changed?
77
80
  end
78
81
  end
@@ -77,10 +77,13 @@ module NSWTopo
77
77
  *, status = Open3.capture2e *%W[taskkill /f /t /pid #{pid}]
78
78
  Process.kill "KILL", pid unless status.success?
79
79
  else
80
- Timeout.timeout(TIMEOUT_KILL, Error) do
81
- Process.kill "-USR1", Process.getpgid(pid)
82
- Process.wait pid
83
- rescue Error
80
+ Process.kill "-USR1", Process.getpgid(pid)
81
+ Enumerator.produce(Time.now) do |time|
82
+ sleep 0.05 and Time.now
83
+ end.each.with_object(Time.now) do |time, start|
84
+ break true if Process.wait pid, Process::WNOHANG
85
+ break false if time - start > TIMEOUT_KILL
86
+ end or begin
84
87
  Process.kill "-KILL", Process.getpgid(pid)
85
88
  Process.wait pid
86
89
  end
@@ -108,8 +111,8 @@ module NSWTopo
108
111
  @id, @data_dir = 0, Dir.mktmpdir("nswtopo_headless_chrome_")
109
112
  ObjectSpace.define_finalizer self, Chrome.rmdir(@data_dir)
110
113
 
111
- args << "--disable-gpu" if Config["gpu"] == false
112
- args << "--user-data-dir=#{@data_dir}"
114
+ args = [*args, "--disable-gpu"] if Config["gpu"] == false
115
+ args = [*args, "--user-data-dir=#{@data_dir}"]
113
116
 
114
117
  input, @input, @output, output = *IO.pipe, *IO.pipe
115
118
  input.nonblock, output.nonblock = false, false
@@ -177,9 +180,14 @@ module NSWTopo
177
180
  raise Error
178
181
  end
179
182
 
180
- def print_to_pdf(pdf_path)
183
+ def print_to_pdf(pdf_path, &block)
181
184
  data = command("Page.printToPDF", timeout: nil, preferCSSPageSize: true).fetch("data")
182
- pdf_path.binwrite Base64.decode64(data)
185
+ pdf = Base64.decode64 data
186
+ if defined? HexaPDF
187
+ HexaPDF::Document.new(io: StringIO.new(pdf)).tap(&block).write(pdf_path.to_s)
188
+ else
189
+ pdf_path.binwrite(pdf)
190
+ end
183
191
  rescue KeyError
184
192
  raise Error
185
193
  end
@@ -6,7 +6,7 @@ module NSWTopo
6
6
  log_warn "no layers installed" if paths.none?
7
7
 
8
8
  TreeIndenter.new(paths) do |paths|
9
- paths.map do |path|
9
+ paths.filter_map do |path|
10
10
  case
11
11
  when path.glob("**/*.yml").any?
12
12
  [path.basename.sub_ext(""), path.children.sort]
@@ -14,7 +14,7 @@ module NSWTopo
14
14
  when path.extname == ".yml"
15
15
  path.basename.sub_ext("")
16
16
  end
17
- end.compact
17
+ end
18
18
  end.each do |indents, name|
19
19
  puts [*indents, name].join
20
20
  end
@@ -1,5 +1,6 @@
1
1
  module NSWTopo
2
2
  module Config
3
+ using Helpers
3
4
  include Log
4
5
  singleton_class.attr_writer :extra_path
5
6
 
@@ -1,5 +1,6 @@
1
1
  module NSWTopo
2
2
  module Formats
3
+ using Helpers
3
4
  def render_gemf(gemf_path, name:, **options, &block)
4
5
  Dir.mktmppath do |temp_dir|
5
6
  ranges = tiled_web_map(temp_dir, **options, extension: "gemf", &block).sort_by do |tile|
@@ -1,5 +1,7 @@
1
1
  module NSWTopo
2
2
  module Formats
3
+ using Helpers
4
+
3
5
  module Kmz
4
6
  TILE_SIZE = 512
5
7
  EARTH_RADIUS = 6378137.0
@@ -50,21 +52,26 @@ module NSWTopo
50
52
  degree_resolution = 180.0 * metre_resolution / Math::PI / Kmz::EARTH_RADIUS
51
53
 
52
54
  wgs84_bounds = @cutline.reproject_to_wgs84.bounds
53
- wgs84_dimensions = wgs84_bounds.transpose.diff / degree_resolution
55
+ wgs84_dimensions = wgs84_bounds.map do |min, max|
56
+ (max - min) / degree_resolution
57
+ end
54
58
 
55
59
  max_zoom = Math::log2(wgs84_dimensions.max).ceil - Math::log2(Kmz::TILE_SIZE).to_i
56
60
  png_path = yield(ppi: ppi)
57
61
 
58
62
  Dir.mktmppath do |temp_dir|
63
+ log_update "kmz: resizing image pyramid"
59
64
  pyramid = (0..max_zoom).map do |zoom|
60
65
  resolution = degree_resolution * 2**(max_zoom - zoom)
61
- degrees_per_tile = resolution * Kmz::TILE_SIZE
62
-
63
66
  tif_path = temp_dir / "#{name}.kmz.zoom.#{zoom}.tif"
67
+ next zoom, resolution, tif_path
68
+ end.inject(ThreadPool.new, &:<<).each do |zoom, resolution, tif_path|
64
69
  OS.gdalwarp "-t_srs", "EPSG:4326", "-tr", resolution, resolution, "-r", "bilinear", "-dstalpha", png_path, tif_path
65
-
70
+ end.map do |zoom, resolution, tif_path|
71
+ degrees_per_tile = resolution * Kmz::TILE_SIZE
66
72
  corners = JSON.parse(OS.gdalinfo "-json", tif_path)["cornerCoordinates"]
67
73
  top_left = corners["upperLeft"]
74
+
68
75
  counts = corners.values.transpose.map(&:minmax).map do |min, max|
69
76
  (max - min) / degrees_per_tile
70
77
  end.map(&:ceil)
@@ -76,9 +83,8 @@ module NSWTopo
76
83
  tile_bounds.each.with_index.entries
77
84
  end.inject(:product).map(&:transpose).map(&:reverse).to_h
78
85
 
79
- log_update "kmz: resizing image pyramid: %i%%" % (100 * (2**(zoom + 1) - 1) / (2**(max_zoom + 1) - 1))
80
- { zoom => [indices_bounds, tif_path] }
81
- end.inject({}, &:merge)
86
+ next zoom, [indices_bounds, tif_path]
87
+ end.to_h
82
88
 
83
89
  kmz_dir = temp_dir.join("#{name}.kmz").tap(&:mkpath)
84
90
  pyramid.flat_map do |zoom, (indices_bounds, tif_path)|
@@ -117,9 +123,9 @@ module NSWTopo
117
123
  end
118
124
  end.tap do |tiles|
119
125
  log_update "kmz: creating %i tiles" % tiles.length
120
- end.each.concurrently do |args|
126
+ end.inject(ThreadPool.new, &:<<).each do |*args|
121
127
  OS.gdal_translate "--config", "GDAL_PAM_ENABLED", "NO", *args
122
- end.map(&:last).each.concurrent_groups do |tile_png_paths|
128
+ end.map(&:last).inject(ThreadPool.new, &:<<).in_groups do |*tile_png_paths|
123
129
  dither *tile_png_paths
124
130
  rescue Dither::Missing
125
131
  end
@@ -129,7 +135,7 @@ module NSWTopo
129
135
  xml.add_element("kml", "xmlns" => "http://earth.google.com/kml/2.1").tap do |kml|
130
136
  kml.add_element("Document").tap do |document|
131
137
  document.add_element("LookAt").tap do |look_at|
132
- extents = @dimensions.times(@scale / 1000.0)
138
+ extents = @dimensions.map { |dimension| dimension * @scale / 1000.0 }
133
139
  range_x = extents.first / 2.0 / Math::tan(Kmz::FOV) / Math::cos(Kmz::TILT)
134
140
  range_y = extents.last / Math::cos(Kmz::FOV - Kmz::TILT) / 2 / (Math::tan(Kmz::FOV - Kmz::TILT) + Math::sin(Kmz::TILT))
135
141
  names_values = [%w[longitude latitude], @centre].transpose
@@ -1,5 +1,6 @@
1
1
  module NSWTopo
2
2
  module Formats
3
+ using Helpers
3
4
  def render_mbtiles(mbtiles_path, name:, **options, &block)
4
5
  wgs84_bounds = @cutline.reproject_to_wgs84.bounds
5
6
  sql = <<~SQL
@@ -1,5 +1,6 @@
1
1
  module NSWTopo
2
2
  module Formats
3
+ using Helpers
3
4
  def render_pdf(pdf_path, ppi: nil, background:, **options)
4
5
  if ppi
5
6
  OS.gdal_translate "-of", "PDF", "-co", "DPI=#{ppi}", "-co", "MARGIN=0", "-co", "CREATOR=nswtopo", "-co", "GEO_ENCODING=ISO32000", yield(ppi: ppi), pdf_path
@@ -10,7 +11,7 @@ module NSWTopo
10
11
 
11
12
  REXML::Document.new(svg_path.read).tap do |xml|
12
13
  xml.elements["svg"].tap do |svg|
13
- style = "@media print { @page { margin: 0; size: %s %s; } }"
14
+ style = "@media print { @page { margin: 0 0 -1mm 0; size: %s %s; } }"
14
15
  svg.add_element("style").text = style % svg.attributes.values_at("width", "height")
15
16
  end
16
17
 
@@ -83,11 +84,40 @@ module NSWTopo
83
84
  svg_path.write xml
84
85
  end
85
86
 
86
- FileUtils.rm pdf_path if pdf_path.exist?
87
87
  log_update "chrome: rendering PDF"
88
-
89
88
  Chrome.with_browser("file://#{svg_path}") do |browser|
90
- browser.print_to_pdf pdf_path
89
+ browser.print_to_pdf(pdf_path) do |doc|
90
+ bbox = [0, 0, dimensions[0] * 72 / 25.4, dimensions[1] * 72 / 25.4]
91
+ bounds = cutline.coordinates[0][...-1].map do |coords|
92
+ coords.zip(dimensions).map { |coord, dimension| coord / dimension }
93
+ end.flatten
94
+ lpts = [0, 0].zip( [1, 1]).inject(&:product).values_at(0,1,3,2).flatten
95
+ gpts = [0, 0].zip(dimensions).inject(&:product).values_at(0,1,3,2).then do |corners|
96
+ # ISO 32000-2 specifies projected coordinates instead of WGS84, but not observed in practice
97
+ GeoJSON.multipoint(corners, projection: projection).reproject_to_wgs84.coordinates.map(&:to_a).map(&:reverse)
98
+ end.flatten
99
+ pcsm = [25.4/72, 0, 0, 0, 25.4/72, 0, 0, 0, 1, 0, 0, 0]
100
+
101
+ doc.pages.first[:VP] = [doc.add({
102
+ Type: :Viewport,
103
+ BBox: bbox,
104
+ Measure: doc.add({
105
+ Type: :Measure,
106
+ Subtype: :GEO,
107
+ Bounds: bounds,
108
+ GCS: doc.add({
109
+ Type: :PROJCS,
110
+ WKT: projection.wkt2
111
+ }),
112
+ GPTS: gpts,
113
+ LPTS: lpts,
114
+ PCSM: pcsm
115
+ })
116
+ })]
117
+
118
+ doc.trailer.info[:Creator] = "nswtopo"
119
+ doc.version = "1.7"
120
+ end
91
121
  end
92
122
  end
93
123
  end
@@ -78,15 +78,15 @@ module NSWTopo
78
78
  layer.empty?
79
79
  end.each do |layer|
80
80
  next if Config["labelling"] == false
81
- labels.add layer if Vector === layer
81
+ labels.add layer if VectorRender === layer
82
82
  end.push(labels).each.with_object [[], []] do |layer, (cutouts, knockouts)|
83
83
  log_update "compositing: #{layer.name}"
84
84
  new_knockouts, knockout = [], "map.mask.knockout.#{knockouts.length+1}"
85
85
  layer.render(cutouts: cutouts, knockout: knockout) do |object|
86
86
  case object
87
- when Labels::Barrier then labels << object
88
- when Vector::Cutout then cutouts << object
89
- when Vector::Knockout then new_knockouts << object
87
+ when Labels::ConvexHulls then labels << object
88
+ when VectorRender::Cutout then cutouts << object
89
+ when VectorRender::Knockout then new_knockouts << object
90
90
  when REXML::Element
91
91
  object.attributes["mask"] ||= "url(#map.mask.knockout.#{knockouts.length})" unless "defs" == object.name
92
92
  yielder << object
@@ -1,5 +1,6 @@
1
1
  module NSWTopo
2
2
  module Formats
3
+ using Helpers
3
4
  def render_svgz(svgz_path, background:, **options)
4
5
  Dir.mktmppath do |temp_dir|
5
6
  svg_path = temp_dir / "svgz-map.svg"
@@ -1,5 +1,6 @@
1
1
  module NSWTopo
2
2
  module Formats
3
+ using Helpers
3
4
  def render_zip(zip_path, name:, ppi: PPI, **options)
4
5
  Dir.mktmppath do |temp_dir|
5
6
  zip_dir = temp_dir.join("zip").tap(&:mkpath)
@@ -8,7 +9,7 @@ module NSWTopo
8
9
 
9
10
  2.downto(0).map.with_index do |level, index|
10
11
  geo_transform = geotransform(ppi: ppi / 2**index)
11
- outsize = (@dimensions / geo_transform[1]).map(&:ceil)
12
+ outsize = @dimensions.map { |dimension| (dimension / geo_transform[1]).ceil }
12
13
  case index
13
14
  when 0
14
15
  thumb_size = outsize.inject(&:<) ? [0, 64] : [64, 0]
@@ -22,7 +23,7 @@ module NSWTopo
22
23
  end
23
24
  img_path = index.zero? ? png_path : temp_dir / "map.#{level}.png"
24
25
  next level, outsize, img_path
25
- end.each.concurrently do |level, outsize, img_path|
26
+ end.inject(ThreadPool.new, &:<<).each do |level, outsize, img_path|
26
27
  OS.gdal_translate *%w[-r bicubic -outsize], *outsize, png_path, img_path unless img_path.exist?
27
28
  end.flat_map do |level, outsize, img_path|
28
29
  outsize.map do |px|
@@ -34,11 +35,11 @@ module NSWTopo
34
35
  end
35
36
  end.tap do |tiles|
36
37
  log_update "zip: creating %i tiles" % tiles.length
37
- end.each.concurrently do |args|
38
+ end.inject(ThreadPool.new, &:<<).each do |*args|
38
39
  OS.gdal_translate *args
39
40
  end.map(&:last).tap do |tile_paths|
40
41
  log_update "zip: optimising %i tiles" % tile_paths.length
41
- end.each.concurrent_groups do |tile_paths|
42
+ end.inject(ThreadPool.new, &:<<).in_groups do |*tile_paths|
42
43
  dither *tile_paths
43
44
  rescue Dither::Missing
44
45
  end
@@ -8,10 +8,13 @@ require_relative 'formats/svgz'
8
8
 
9
9
  module NSWTopo
10
10
  module Formats
11
+ using Helpers
11
12
  include Log
13
+
12
14
  PPI = 300
13
15
  TILE = 1500
14
- ARGS = %w[--force-gpu-mem-available-mb=4096]
16
+ CHROME_ARGS = %w[--force-gpu-mem-available-mb=4096]
17
+ CHROME_INSTANCES = (ThreadPool::CORES / 4).clamp(1, 6)
15
18
 
16
19
  def self.extensions
17
20
  instance_methods.grep(/^render_([a-z]+)/) { $1 }
@@ -69,48 +72,44 @@ module NSWTopo
69
72
  end
70
73
 
71
74
  viewport_size = [TILE * mm_per_px] * 2
72
- raster_size = (@dimensions / mm_per_px).map(&:ceil)
75
+ raster_size = @dimensions.map { |dimension| (dimension / mm_per_px).ceil }
73
76
  megapixels = raster_size.inject(&:*) / 1024.0 / 1024.0
74
77
 
75
78
  raster_info = "%i×%i (%.1fMpx) map raster at %s" % [*raster_size, megapixels, ppi_info]
76
- chrome_message = "chrome: creating #{raster_info}"
77
- log_update chrome_message
79
+ log_update "chrome: creating #{raster_info}"
78
80
 
79
- NSWTopo::Chrome.with_browser "file://#{svg_path}", width: TILE, height: TILE, args: ARGS do |browser|
80
- svg = browser.query_selector "svg"
81
- svg[:width], svg[:height] = nil, nil
82
- svg[:viewBox].split.map(&:to_f).last(2).map do |mm|
83
- (0...(mm / mm_per_px).ceil).step(TILE).map do |px|
84
- [px, px * mm_per_px]
81
+ REXML::Document.new(svg_path.read).elements["svg/@viewBox"].value.split.map(&:to_f).last(2).map do |mm|
82
+ (0...(mm / mm_per_px).ceil).step(TILE).map do |px|
83
+ [px, px * mm_per_px]
84
+ end
85
+ end.inject(&:product).map(&:transpose).map do |raster_offset, viewport_offset|
86
+ next raster_offset, viewport_offset, temp_dir.join("tile.%i.%i.png" % raster_offset)
87
+ end.inject(ThreadPool.new(CHROME_INSTANCES), &:<<).in_groups do |*grid|
88
+ NSWTopo::Chrome.with_browser "file://#{svg_path}", width: TILE, height: TILE, args: CHROME_ARGS do |browser|
89
+ svg = browser.query_selector "svg"
90
+ svg[:width], svg[:height] = nil, nil
91
+ grid.each do |raster_offset, viewport_offset, tile_path|
92
+ svg[:viewBox] = [*viewport_offset, *viewport_size].join(?\s)
93
+ browser.screenshot tile_path
85
94
  end
86
- end.inject(&:product).map(&:transpose).tap do |grid|
87
- chrome_message += " (tile %i of #{grid.size})"
88
- end.map.with_index do |(raster_offset, viewport_offset), index|
89
- log_update chrome_message % [index + 1]
90
-
91
- tile_path = temp_dir.join("tile.%i.%i.png" % raster_offset)
92
- viewbox = [*viewport_offset, *viewport_size].join(?\s)
93
-
94
- svg[:viewBox] = viewbox
95
- browser.screenshot tile_path
96
-
97
- REXML::Document.new(OS.gdal_translate "-of", "VRT", tile_path, "/vsistdout/").tap do |vrt|
98
- vrt.elements.each("VRTDataset/VRTRasterBand/SimpleSource/DstRect") do |dst_rect|
99
- dst_rect.add_attributes "xOff" => raster_offset[0], "yOff" => raster_offset[1]
100
- end
95
+ end
96
+ end.map do |raster_offset, viewport_offset, tile_path|
97
+ REXML::Document.new(OS.gdal_translate "-of", "VRT", tile_path, "/vsistdout/").tap do |vrt|
98
+ vrt.elements.each("VRTDataset/VRTRasterBand/SimpleSource/DstRect") do |dst_rect|
99
+ dst_rect.add_attributes "xOff" => raster_offset[0], "yOff" => raster_offset[1]
101
100
  end
102
- end.inject do |vrt, tile_vrt|
103
- vrt.elements["VRTDataset/VRTRasterBand[@band='1']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='1']/SimpleSource"]
104
- vrt.elements["VRTDataset/VRTRasterBand[@band='2']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='2']/SimpleSource"]
105
- vrt.elements["VRTDataset/VRTRasterBand[@band='3']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='3']/SimpleSource"]
106
- vrt.elements["VRTDataset/VRTRasterBand[@band='4']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='4']/SimpleSource"]
107
- vrt
108
- end.tap do |vrt|
109
- vrt.elements.each("VRTDataset/VRTRasterBand/@blockYSize", &:remove)
110
- vrt.elements.each("VRTDataset/Metadata", &:remove)
111
- vrt.elements["VRTDataset"].add_attributes "rasterXSize" => raster_size[0], "rasterYSize" => raster_size[1]
112
- File.write vrt_path, vrt
113
101
  end
102
+ end.inject do |vrt, tile_vrt|
103
+ vrt.elements["VRTDataset/VRTRasterBand[@band='1']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='1']/SimpleSource"]
104
+ vrt.elements["VRTDataset/VRTRasterBand[@band='2']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='2']/SimpleSource"]
105
+ vrt.elements["VRTDataset/VRTRasterBand[@band='3']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='3']/SimpleSource"]
106
+ vrt.elements["VRTDataset/VRTRasterBand[@band='4']"].add_element tile_vrt.elements["VRTDataset/VRTRasterBand[@band='4']/SimpleSource"]
107
+ vrt
108
+ end.tap do |vrt|
109
+ vrt.elements.each("VRTDataset/VRTRasterBand/@blockYSize", &:remove)
110
+ vrt.elements.each("VRTDataset/Metadata", &:remove)
111
+ vrt.elements["VRTDataset"].add_attributes "rasterXSize" => raster_size[0], "rasterYSize" => raster_size[1]
112
+ File.write vrt_path, vrt
114
113
  end
115
114
 
116
115
  log_update "nswtopo: finalising #{raster_info}"
@@ -1,46 +1,47 @@
1
1
  class RTree
2
+ using Helpers
3
+
2
4
  def initialize(nodes, bounds, object = nil)
3
5
  @nodes, @bounds, @object = nodes, bounds, object
4
6
  end
5
7
 
8
+ attr_reader :bounds
9
+
6
10
  def overlaps?(bounds, buffer)
7
11
  return false if @bounds.empty?
8
12
  return true unless bounds
9
- bounds.zip(@bounds).all? do |bound1, bound2|
10
- bound1.zip(bound2.rotate).each.with_index.all? do |limits, index|
11
- limits.rotate(index).inject(&:-) <= buffer
12
- end
13
+ bounds.zip(@bounds).all? do |(min1, max1), (min2, max2)|
14
+ max1 + buffer >= min2 && max2 + buffer >= min1
13
15
  end
14
16
  end
15
17
 
16
- def self.load(bounds_objects, &block)
17
- case
18
- when block_given? then load bounds_objects.map(&block).zip(bounds_objects)
19
- when bounds_objects.one? then RTree.new [], *bounds_objects.first
20
- else
21
- nodes = bounds_objects.sort_by do |bounds, object|
22
- bounds[0].inject(&:+)
23
- end.in_two.map do |bounds_objects|
24
- bounds_objects.sort_by do |bounds, object|
25
- bounds[1].inject(&:+)
26
- end.in_two.map do |bounds_objects|
27
- load bounds_objects
28
- end
29
- end.flatten
30
- RTree.new nodes, bounds_objects.map(&:first).transpose.map(&:flatten).map(&:minmax)
18
+ def self.load(objects, &bounds)
19
+ load! objects.map(&bounds).zip(objects)
20
+ end
21
+
22
+ def self.load!(bounds_objects, range = 0...bounds_objects.length)
23
+ return RTree.new([], *bounds_objects[range.begin]) if range.one?
24
+ bounds_objects.median_partition!(range) do |bounds, object|
25
+ bounds[0].sum
26
+ end.flat_map do |range|
27
+ bounds_objects.median_partition!(range) do |bounds, object|
28
+ bounds[1].sum
29
+ end
30
+ end.filter_map do |range|
31
+ load!(bounds_objects, range) if range.any?
32
+ end.then do |nodes|
33
+ RTree.new nodes, nodes.map(&:bounds).transpose.map(&:flatten).map(&:minmax)
31
34
  end
32
35
  end
33
36
 
34
- def search(bounds, buffer: 0, searched: Set.new)
37
+ def search(bounds, buffer = 0)
35
38
  Enumerator.new do |yielder|
36
- next if searched.include? self
37
39
  if overlaps? bounds, buffer
38
40
  @nodes.each do |node|
39
- node.search(bounds, buffer: buffer, searched: searched).inject(yielder, &:<<)
41
+ node.search(bounds, buffer).each(&yielder)
40
42
  end
41
43
  yielder << @object if @nodes.empty?
42
44
  end
43
- searched << self
44
45
  end
45
46
  end
46
47
 
@@ -39,11 +39,11 @@ module StraightSkeleton
39
39
  def project(travel)
40
40
  det = normals.inject(&:cross) if normals.all?
41
41
  case
42
- when det && det.nonzero?
42
+ when det&.nonzero?
43
43
  x = normals.map { |normal| travel - @travel + normal.dot(point) }
44
- [normals[1][1] * x[0] - normals[0][1] * x[1], normals[0][0] * x[1] - normals[1][0] * x[0]] / det
45
- when normals[0] then normals[0].times(travel - @travel).plus(point)
46
- when normals[1] then normals[1].times(travel - @travel).plus(point)
44
+ (normals[0].perp * x[1] - normals[1].perp * x[0]) / det
45
+ when normals[0] then normals[0] * (travel - @travel) + point
46
+ when normals[1] then normals[1] * (travel - @travel) + point
47
47
  end
48
48
  end
49
49
  end