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.
- checksums.yaml +4 -4
- data/bin/nswtopo +20 -4
- data/docs/contours.md +2 -0
- data/docs/relief.md +2 -3
- data/docs/spot-heights.md +2 -0
- data/lib/nswtopo/archive.rb +6 -3
- data/lib/nswtopo/chrome.rb +9 -6
- data/lib/nswtopo/commands/layers.rb +2 -2
- data/lib/nswtopo/config.rb +1 -0
- data/lib/nswtopo/formats/gemf.rb +1 -0
- data/lib/nswtopo/formats/kmz.rb +16 -10
- data/lib/nswtopo/formats/mbtiles.rb +1 -0
- data/lib/nswtopo/formats/pdf.rb +4 -3
- data/lib/nswtopo/formats/svg.rb +5 -13
- data/lib/nswtopo/formats/svgz.rb +1 -0
- data/lib/nswtopo/formats/zip.rb +5 -4
- data/lib/nswtopo/formats.rb +35 -36
- data/lib/nswtopo/geometry/r_tree.rb +24 -23
- data/lib/nswtopo/geometry/straight_skeleton/node.rb +4 -4
- data/lib/nswtopo/geometry/straight_skeleton/nodes.rb +51 -40
- data/lib/nswtopo/geometry/straight_skeleton/split.rb +2 -2
- data/lib/nswtopo/geometry/vector.rb +55 -49
- data/lib/nswtopo/geometry.rb +0 -5
- data/lib/nswtopo/gis/arcgis/layer/map.rb +11 -10
- data/lib/nswtopo/gis/arcgis/layer/query.rb +8 -10
- data/lib/nswtopo/gis/arcgis/layer.rb +7 -11
- data/lib/nswtopo/gis/dem.rb +3 -2
- data/lib/nswtopo/gis/gdal_glob.rb +3 -3
- data/lib/nswtopo/gis/geojson/collection.rb +60 -14
- data/lib/nswtopo/gis/geojson/line_string.rb +142 -1
- data/lib/nswtopo/gis/geojson/multi_line_string.rb +49 -7
- data/lib/nswtopo/gis/geojson/multi_point.rb +87 -0
- data/lib/nswtopo/gis/geojson/multi_polygon.rb +35 -23
- data/lib/nswtopo/gis/geojson/point.rb +16 -1
- data/lib/nswtopo/gis/geojson/polygon.rb +69 -7
- data/lib/nswtopo/gis/geojson.rb +92 -46
- data/lib/nswtopo/gis/projection.rb +5 -1
- data/lib/nswtopo/helpers/thread_pool.rb +39 -0
- data/lib/nswtopo/helpers.rb +44 -5
- data/lib/nswtopo/layer/arcgis_raster.rb +4 -6
- data/lib/nswtopo/layer/contour.rb +24 -26
- data/lib/nswtopo/layer/control.rb +5 -3
- data/lib/nswtopo/layer/declination.rb +14 -10
- data/lib/nswtopo/layer/feature.rb +5 -5
- data/lib/nswtopo/layer/grid.rb +19 -18
- data/lib/nswtopo/layer/labels/barriers.rb +23 -0
- data/lib/nswtopo/layer/labels/convex_hull.rb +12 -0
- data/lib/nswtopo/layer/labels/convex_hulls.rb +86 -0
- data/lib/nswtopo/layer/labels/label.rb +63 -0
- data/lib/nswtopo/layer/labels.rb +192 -315
- data/lib/nswtopo/layer/overlay.rb +11 -12
- data/lib/nswtopo/layer/raster.rb +1 -0
- data/lib/nswtopo/layer/relief.rb +6 -4
- data/lib/nswtopo/layer/spot.rb +11 -17
- data/lib/nswtopo/layer/{vector → vector_render}/cutout.rb +1 -1
- data/lib/nswtopo/layer/{vector → vector_render}/knockout.rb +2 -3
- data/lib/nswtopo/layer/{vector.rb → vector_render.rb} +20 -45
- data/lib/nswtopo/layer.rb +2 -1
- data/lib/nswtopo/map.rb +70 -56
- data/lib/nswtopo/svg.rb +5 -0
- data/lib/nswtopo/tiled_web_map.rb +3 -3
- data/lib/nswtopo/tree_indenter.rb +2 -2
- data/lib/nswtopo/version.rb +1 -1
- data/lib/nswtopo.rb +4 -0
- metadata +15 -17
- data/lib/nswtopo/geometry/overlap.rb +0 -47
- data/lib/nswtopo/geometry/segment.rb +0 -27
- data/lib/nswtopo/geometry/vector_sequence.rb +0 -180
- data/lib/nswtopo/helpers/array.rb +0 -19
- data/lib/nswtopo/helpers/concurrently.rb +0 -27
- data/lib/nswtopo/helpers/dir.rb +0 -7
- data/lib/nswtopo/helpers/hash.rb +0 -15
- data/lib/nswtopo/helpers/tar_writer.rb +0 -11
- data/lib/nswtopo/layer/labels/barrier.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be473c574c562b474c1d4ef9b3a1dfc2c697993b1034990375f1405ebfabf1cb
|
4
|
+
data.tar.gz: 7b230fbd3f2e5164bb6452c10a15cd38424dbeea29c42947f6e36150a5784570
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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-
|
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,
|
41
|
-
log_abort "ruby 3.
|
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:
|
data/lib/nswtopo/archive.rb
CHANGED
@@ -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
|
-
|
75
|
-
|
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
|
data/lib/nswtopo/chrome.rb
CHANGED
@@ -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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
112
|
-
args
|
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.
|
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
|
17
|
+
end
|
18
18
|
end.each do |indents, name|
|
19
19
|
puts [*indents, name].join
|
20
20
|
end
|
data/lib/nswtopo/config.rb
CHANGED
data/lib/nswtopo/formats/gemf.rb
CHANGED
data/lib/nswtopo/formats/kmz.rb
CHANGED
@@ -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.
|
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
|
-
|
80
|
-
|
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.
|
126
|
+
end.inject(ThreadPool.new, &:<<).each do |*args|
|
121
127
|
OS.gdal_translate "--config", "GDAL_PAM_ENABLED", "NO", *args
|
122
|
-
end.map(&:last).
|
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.
|
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
|
data/lib/nswtopo/formats/pdf.rb
CHANGED
@@ -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
|
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
|
-
#
|
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
|
|
data/lib/nswtopo/formats/svg.rb
CHANGED
@@ -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" =>
|
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
|
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::
|
88
|
-
when
|
89
|
-
when
|
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
|
data/lib/nswtopo/formats/svgz.rb
CHANGED
data/lib/nswtopo/formats/zip.rb
CHANGED
@@ -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 =
|
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.
|
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.
|
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.
|
42
|
+
end.inject(ThreadPool.new, &:<<).in_groups do |*tile_paths|
|
42
43
|
dither *tile_paths
|
43
44
|
rescue Dither::Missing
|
44
45
|
end
|
data/lib/nswtopo/formats.rb
CHANGED
@@ -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
|
-
|
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 =
|
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
|
-
|
77
|
-
log_update chrome_message
|
79
|
+
log_update "chrome: creating #{raster_info}"
|
78
80
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
87
|
-
|
88
|
-
|
89
|
-
|
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 |
|
10
|
-
|
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(
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
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
|
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
|
42
|
+
when det&.nonzero?
|
43
43
|
x = normals.map { |normal| travel - @travel + normal.dot(point) }
|
44
|
-
|
45
|
-
when normals[0] then normals[0]
|
46
|
-
when normals[1] then normals[1]
|
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
|