nswtopo 3.0.1 → 3.1.1

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/bin/nswtopo +20 -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 +9 -6
  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 +4 -3
  14. data/lib/nswtopo/formats/svg.rb +5 -13
  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 +60 -14
  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 +4 -6
  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 +70 -56
  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 +4 -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: cbc36bc0fcdc3467aad475f9ff3757bab270bba4fe81e7bed3c34b0c34a3b4f2
4
- data.tar.gz: fb02ea3f103222c9424bbe9f1c750caa608f73e4e5815687f40c1a383f6fa8d9
3
+ metadata.gz: be473c574c562b474c1d4ef9b3a1dfc2c697993b1034990375f1405ebfabf1cb
4
+ data.tar.gz: 7b230fbd3f2e5164bb6452c10a15cd38424dbeea29c42947f6e36150a5784570
5
5
  SHA512:
6
- metadata.gz: 06a794c2eaa6949d4754c62408d6d3358ee1d84c9dcfb10a2ab90ec9833b3916a84f5873f06657b5bca4e328765848d53ff1dafb150a58e42cd15085808e1e43
7
- data.tar.gz: 83325ab407ea8a7fcc6675cfcb747a8d1f08932d228dbdcb18fa8dec48d328a38690dcbb0766be975f03d7b872caf6e60ef0706a5429713c8a86b09df16cecaa
6
+ metadata.gz: a925a874ddb9802d539712371e6dd06e28cb4a3fbdcd22d6b96a274131ae3a89244180d926d9f10a785db14ca98e0fdac4842a9766dad24475f3ff9c878e3c68
7
+ data.tar.gz: 145391125d1035c7bc5dc3b127389acc846277435624e971dcc82033089d784f5f84c0617ff7cc2af35884d750858b13d433185c73acb0fdd0eb363af9077393
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
@@ -212,11 +220,13 @@ begin
212
220
  parser.on "-b", "--bounds <bounds.kml>", Pathname, "bounds for map as KML or GPX file"
213
221
  parser.on "-c", "--coords <x1,y1,...>", CoordList, "bounds for map as one or more WGS84",
214
222
  "longitude/latitude pairs"
223
+ parser.on "-n", "--neatline <neatline.kml>", Pathname, "neatline for map as KML file"
215
224
  parser.on "-d", "--dimensions <width,height>", Dimensions, "map dimensions in mm"
216
225
  parser.on "-r", "--rotation <rotation>", Rotation, "map rotation angle in clockwise",
217
226
  "degrees, 'auto' or 'magnetic'"
218
227
  parser.on "-m", "--margins <x[,y]>", Margins, "map margins in mm"
219
228
  parser.on "-i", "--inset <x1,y1,x2,y2>", Inset, "map inset coordinates in mm"
229
+ parser.on "--radius <radius>", Radius, "map corner radius in mm"
220
230
  parser.on "-o", "--overwrite", "overwrite existing map file"
221
231
 
222
232
  when "info"
@@ -265,6 +275,7 @@ begin
265
275
  parser.on "--stroke-width <width>", PositiveFloat, "stroke width in mm (default %s)" % NSWTopo::Contour::DEFAULTS["stroke-width"]
266
276
  parser.on "--fill <colour>", Colour, "label colour (defaults to stroke colour)"
267
277
  parser.on "-r", "--resolution <resolution>", PositiveFloat, "DEM processing resolution in metres"
278
+ parser.on "--epsg <epsg>", PositiveInt, "override EPSG projection code for DEM"
268
279
 
269
280
  when "spot-heights"
270
281
  parser.banner = <<~EOF
@@ -279,6 +290,7 @@ begin
279
290
  parser.on "-b", "--before <layer>", "insert before specified layer"
280
291
  parser.on "-c", "--replace <layer>", "replace specified layer"
281
292
  parser.on "-r", "--resolution <resolution>", PositiveFloat, "DEM processing resolution in metres"
293
+ parser.on "--epsg <epsg>", PositiveInt, "override EPSG projection code for DEM"
282
294
 
283
295
  when "relief"
284
296
  parser.banner = <<~EOF
@@ -292,6 +304,7 @@ begin
292
304
  parser.on "-m", "--method <igor|combined>", %w[igor combined], "relief shading method (default %s)" % NSWTopo::Relief::DEFAULTS["method"]
293
305
  parser.on "-z", "--azimuth <azimuth>", Float, "azimuth in degrees (default %i)" % NSWTopo::Relief::DEFAULTS["azimuth"]
294
306
  parser.on "-f", "--factor <factor>", PositiveFloat, "exaggeration factor (default %s)" % NSWTopo::Relief::DEFAULTS["factor"]
307
+ parser.on "--epsg <epsg>", PositiveInt, "override EPSG projection code for DEM"
295
308
 
296
309
  when "grid"
297
310
  parser.banner = <<~EOF
@@ -538,4 +551,7 @@ rescue Interrupt
538
551
  log_abort "interrupted"
539
552
  rescue RuntimeError => error
540
553
  log_abort error.message
554
+ rescue StandardError
555
+ print "\r\e[2K" if $stdout.tty?
556
+ raise
541
557
  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
@@ -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
@@ -86,14 +87,14 @@ module NSWTopo
86
87
  log_update "chrome: rendering PDF"
87
88
  Chrome.with_browser("file://#{svg_path}") do |browser|
88
89
  browser.print_to_pdf(pdf_path) do |doc|
89
- bbox = [0, 0, *dimensions.times(72/25.4)]
90
+ bbox = [0, 0, dimensions[0] * 72 / 25.4, dimensions[1] * 72 / 25.4]
90
91
  bounds = cutline.coordinates[0][...-1].map do |coords|
91
92
  coords.zip(dimensions).map { |coord, dimension| coord / dimension }
92
93
  end.flatten
93
94
  lpts = [0, 0].zip( [1, 1]).inject(&:product).values_at(0,1,3,2).flatten
94
95
  gpts = [0, 0].zip(dimensions).inject(&:product).values_at(0,1,3,2).then do |corners|
95
- # corners.map(&:reverse) # ISO 32000-2 specifies this, but not observed in practice
96
- GeoJSON.multipoint(corners, projection: projection).reproject_to_wgs84.coordinates.map(&:reverse)
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)
97
98
  end.flatten
98
99
  pcsm = [25.4/72, 0, 0, 0, 25.4/72, 0, 0, 0, 1, 0, 0, 0]
99
100
 
@@ -17,14 +17,6 @@ module NSWTopo
17
17
  end
18
18
 
19
19
  module Formats
20
- def neatline_path_data
21
- @neatline.coordinates.map do |ring|
22
- ring.map do |point|
23
- point.join(" ")
24
- end.join(" L ").prepend("M ").concat(" Z")
25
- end.join(" ")
26
- end
27
-
28
20
  def render_svg(svg_path, background:, **options)
29
21
  if uptodate?("map.svg", "map.yml")
30
22
  log_update "nswtopo: reading existing map SVG"
@@ -63,7 +55,7 @@ module NSWTopo
63
55
  # add defs for map filters and masks
64
56
  defs = svg.add_element("defs", "id" => "map.defs")
65
57
  defs.add_element("rect", "id" => "map.rect", "width" => width, "height" => height)
66
- defs.add_element("path", "id" => "map.neatline", "d" => neatline_path_data)
58
+ defs.add_element("path", "id" => "map.neatline", "d" => @neatline.svg_path_data)
67
59
  defs.add_element("clipPath", "id" => "map.clip").add_element("use", "href" => "#map.neatline")
68
60
 
69
61
  # add a filter converting alpha channel to cutout mask
@@ -78,15 +70,15 @@ module NSWTopo
78
70
  layer.empty?
79
71
  end.each do |layer|
80
72
  next if Config["labelling"] == false
81
- labels.add layer if Vector === layer
73
+ labels.add layer if VectorRender === layer
82
74
  end.push(labels).each.with_object [[], []] do |layer, (cutouts, knockouts)|
83
75
  log_update "compositing: #{layer.name}"
84
76
  new_knockouts, knockout = [], "map.mask.knockout.#{knockouts.length+1}"
85
77
  layer.render(cutouts: cutouts, knockout: knockout) do |object|
86
78
  case object
87
- when Labels::Barrier then labels << object
88
- when Vector::Cutout then cutouts << object
89
- when Vector::Knockout then new_knockouts << object
79
+ when Labels::ConvexHulls then labels << object
80
+ when VectorRender::Cutout then cutouts << object
81
+ when VectorRender::Knockout then new_knockouts << object
90
82
  when REXML::Element
91
83
  object.attributes["mask"] ||= "url(#map.mask.knockout.#{knockouts.length})" unless "defs" == object.name
92
84
  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
+ raster_size.map do |px|
82
+ (0...px).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