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
@@ -1,32 +1,31 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module Overlay
|
3
|
-
include
|
3
|
+
include VectorRender
|
4
4
|
CREATE = %w[simplify tolerance]
|
5
5
|
TOLERANCE = 0.4
|
6
6
|
|
7
7
|
GPX_STYLES = YAML.load <<~YAML
|
8
8
|
stroke: black
|
9
9
|
stroke-width: 0.4
|
10
|
+
barrier: true
|
10
11
|
YAML
|
11
12
|
|
12
13
|
def get_features
|
13
14
|
GPS.new(@path).tap do |gps|
|
14
15
|
@simplify = true if GPS::GPX === gps
|
15
16
|
@tolerance ||= [@map.to_mm(5), TOLERANCE].max if @simplify
|
16
|
-
end.collection.reproject_to(@map.neatline.projection).explode.
|
17
|
+
end.collection.reproject_to(@map.neatline.projection).explode.map! do |feature|
|
18
|
+
if @tolerance && GeoJSON::LineString === feature
|
19
|
+
feature.simplify(@tolerance).segmentise(2*@tolerance).smooth_window(3)
|
20
|
+
else
|
21
|
+
feature
|
22
|
+
end
|
23
|
+
end.map! do |feature|
|
17
24
|
styles, folder, name = feature.values_at "styles", "folder", "name"
|
18
25
|
styles ||= GPX_STYLES
|
19
|
-
|
20
26
|
case feature
|
21
27
|
when GeoJSON::LineString
|
22
28
|
styles["stroke-linejoin"] = "round"
|
23
|
-
if @tolerance
|
24
|
-
simplified = feature.coordinates.douglas_peucker(@tolerance)
|
25
|
-
smoothed = simplified.sample_at(2*@tolerance).each_cons(2).map do |segment|
|
26
|
-
segment.along(0.5)
|
27
|
-
end.push(simplified.last).prepend(simplified.first)
|
28
|
-
feature.coordinates = smoothed
|
29
|
-
end
|
30
29
|
when GeoJSON::Polygon
|
31
30
|
styles["stroke-linejoin"] = "miter"
|
32
31
|
end
|
@@ -34,10 +33,10 @@ module NSWTopo
|
|
34
33
|
categories = [folder, name].compact.reject(&:empty?).map(&method(:categorise))
|
35
34
|
keys = styles.keys - params_for(categories.to_set).keys
|
36
35
|
styles = styles.slice *keys
|
36
|
+
categories << feature.object_id
|
37
37
|
|
38
|
-
feature.clear
|
39
|
-
feature["category"] = categories << feature.object_id
|
40
38
|
@params[categories.join(?\s)] = styles if styles.any?
|
39
|
+
feature.with_properties("category" => categories)
|
41
40
|
end
|
42
41
|
end
|
43
42
|
|
data/lib/nswtopo/layer/raster.rb
CHANGED
data/lib/nswtopo/layer/relief.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module Relief
|
3
3
|
include Raster, MaskRender, DEM, Log
|
4
|
-
CREATE = %w[method azimuth factor smooth contours]
|
4
|
+
CREATE = %w[method azimuth factor smooth contours epsg]
|
5
5
|
DEFAULTS = YAML.load <<~YAML
|
6
6
|
shade: rgb(0,0,48)
|
7
7
|
method: combined
|
@@ -26,7 +26,9 @@ module NSWTopo
|
|
26
26
|
when @contours
|
27
27
|
bounds = cutline.bounds
|
28
28
|
raise "no resolution specified for #{@name}" unless Numeric === @mm_per_px
|
29
|
-
outsize =
|
29
|
+
outsize = bounds.map do |min, max|
|
30
|
+
(max - min) / @mm_per_px
|
31
|
+
end.map(&:ceil)
|
30
32
|
|
31
33
|
collection = @contours.map do |url_or_path, attribute_or_hash|
|
32
34
|
raise "no elevation attribute specified for #{url_or_path}" unless attribute_or_hash
|
@@ -42,8 +44,8 @@ module NSWTopo
|
|
42
44
|
Shapefile::Source.new(url_or_path).layer(**options, geometry: cutline).features
|
43
45
|
else
|
44
46
|
raise "unrecognised elevation data source: #{url_or_path}"
|
45
|
-
end.
|
46
|
-
feature.
|
47
|
+
end.map! do |feature|
|
48
|
+
feature.with_properties("elevation" => feature.fetch(attribute, attribute).to_f)
|
47
49
|
end.reproject_to(@map.projection)
|
48
50
|
end.inject(&:merge)
|
49
51
|
|
data/lib/nswtopo/layer/spot.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
module Spot
|
3
|
-
|
4
|
-
|
3
|
+
using Helpers
|
4
|
+
include VectorRender, DEM, Log
|
5
|
+
|
6
|
+
CREATE = %w[spacing smooth prefer extent epsg]
|
5
7
|
DEFAULTS = YAML.load <<~YAML
|
6
8
|
spacing: 15
|
7
9
|
smooth: 0.2
|
@@ -39,7 +41,7 @@ module NSWTopo
|
|
39
41
|
end
|
40
42
|
|
41
43
|
module Candidate
|
42
|
-
attr_accessor :elevation, :knoll
|
44
|
+
attr_accessor :elevation, :knoll, :conflicts
|
43
45
|
|
44
46
|
module PreferKnolls
|
45
47
|
def ordinal; [conflicts.size, -elevation] end
|
@@ -53,17 +55,9 @@ module NSWTopo
|
|
53
55
|
def ordinal; conflicts.size end
|
54
56
|
end
|
55
57
|
|
56
|
-
def conflicts
|
57
|
-
@conflicts ||= Set[]
|
58
|
-
end
|
59
|
-
|
60
58
|
def <=>(other)
|
61
59
|
self.ordinal <=> other.ordinal
|
62
60
|
end
|
63
|
-
|
64
|
-
def bounds(buffer: 0)
|
65
|
-
coordinates.map { |coordinate| [coordinate - buffer, coordinate + buffer] }
|
66
|
-
end
|
67
61
|
end
|
68
62
|
|
69
63
|
def ordering
|
@@ -90,7 +84,7 @@ module NSWTopo
|
|
90
84
|
aspect.ncols.times do |col|
|
91
85
|
offsets.map!(&:next)
|
92
86
|
next if row < 1 || col < 1 || row >= aspect.nrows - 1 || col >= aspect.ncols - 1
|
93
|
-
next if block
|
87
|
+
next if block_given? && block.call(col, row)
|
94
88
|
ccw, cw = offsets.each_cons(2).inject([true, true]) do |(ccw, cw), (o1, o2)|
|
95
89
|
break unless ccw || cw
|
96
90
|
a1, a2 = aspect.values.values_at o1, o2
|
@@ -124,15 +118,15 @@ module NSWTopo
|
|
124
118
|
pixels, knolls = pixels_knolls(dem_hr_path) do |col, row|
|
125
119
|
!mask.include? [(col * @mm_per_px / low_resolution).floor, (row * @mm_per_px / low_resolution).floor]
|
126
120
|
end.entries.transpose
|
121
|
+
raise "no elevation data found in map area" unless pixels
|
127
122
|
|
128
123
|
locations = raster_locations dem_hr_path, pixels
|
129
124
|
elevations = raster_values dem_hr_path, pixels
|
130
125
|
|
131
126
|
locations.zip(elevations, knolls).map do |coordinates, elevation, knoll|
|
132
|
-
GeoJSON::Point
|
127
|
+
GeoJSON::Point[coordinates, "label" => elevation.round] do |feature|
|
133
128
|
feature.extend Candidate, ordering
|
134
|
-
feature.knoll, feature.elevation = knoll, elevation
|
135
|
-
feature["label"] = elevation.round
|
129
|
+
feature.knoll, feature.elevation, feature.conflicts = knoll, elevation, Set[]
|
136
130
|
end
|
137
131
|
end
|
138
132
|
end
|
@@ -144,9 +138,9 @@ module NSWTopo
|
|
144
138
|
|
145
139
|
candidates.each.with_index do |candidate, index|
|
146
140
|
log_update "%s: examining candidates: %.1f%%" % [@name, 100.0 * index / candidates.length]
|
147
|
-
spatial_index.search(candidate.bounds
|
141
|
+
spatial_index.search(candidate.bounds, @spacing).each do |other|
|
148
142
|
next if other == candidate
|
149
|
-
next if
|
143
|
+
next if (candidate.coordinates - other.coordinates).norm > @spacing
|
150
144
|
candidate.conflicts << other
|
151
145
|
end
|
152
146
|
end.each do |candidate|
|
@@ -1,9 +1,8 @@
|
|
1
1
|
module NSWTopo
|
2
|
-
module
|
2
|
+
module VectorRender
|
3
3
|
class Knockout
|
4
4
|
def initialize(element, buffer)
|
5
|
-
buffer =
|
6
|
-
@buffer = Float(buffer)
|
5
|
+
@buffer = Labels::Label.knockout(buffer)
|
7
6
|
@href = "#" + element.attributes["id"]
|
8
7
|
end
|
9
8
|
attr_reader :buffer
|
@@ -1,8 +1,11 @@
|
|
1
|
-
require_relative '
|
2
|
-
require_relative '
|
1
|
+
require_relative 'vector_render/cutout'
|
2
|
+
require_relative 'vector_render/knockout'
|
3
3
|
|
4
4
|
module NSWTopo
|
5
|
-
module
|
5
|
+
module VectorRender
|
6
|
+
using Helpers
|
7
|
+
include SVG
|
8
|
+
|
6
9
|
SVG_ATTRIBUTES = %w[
|
7
10
|
fill-opacity
|
8
11
|
fill
|
@@ -39,7 +42,6 @@ module NSWTopo
|
|
39
42
|
|
40
43
|
SHIELD_X, SHIELD_Y = 1.0, 0.5
|
41
44
|
MARGIN = { mm: 1.0 }
|
42
|
-
VALUE, POINT, ANGLE = "%.5f", "%.5f %.5f", "%.2f"
|
43
45
|
|
44
46
|
def create
|
45
47
|
@features = get_features.reproject_to(@map.neatline.projection).clip(@map.neatline(**MARGIN))
|
@@ -78,28 +80,6 @@ module NSWTopo
|
|
78
80
|
string.tr_s('^_a-zA-Z0-9', ?-).delete_prefix(?-).delete_suffix(?-)
|
79
81
|
end
|
80
82
|
|
81
|
-
def svg_path_data(points, bezier: false)
|
82
|
-
if bezier
|
83
|
-
fraction = Numeric === bezier ? bezier.clamp(0.0, 1.0) : 1.0
|
84
|
-
extras = points.first == points.last ? [points[-2], *points, points[2]] : [points.first, *points, points.last]
|
85
|
-
midpoints = extras.segments.map(&:midpoint)
|
86
|
-
distances = extras.segments.map(&:distance)
|
87
|
-
offsets = midpoints.zip(distances).segments.map(&:transpose).map do |segment, distance|
|
88
|
-
segment.along(distance.first / distance.inject(&:+))
|
89
|
-
end.zip(points).map(&:diff)
|
90
|
-
controls = midpoints.segments.zip(offsets).flat_map do |segment, offset|
|
91
|
-
segment.map { |point| [point, point.plus(offset)].along(fraction) }
|
92
|
-
end.drop(1).each_slice(2).entries.prepend(nil)
|
93
|
-
points.zip(controls).map do |point, controls|
|
94
|
-
controls ? "C %s %s %s" % [POINT, POINT, POINT] % [*controls.flatten, *point] : "M %s" % POINT % point
|
95
|
-
end.join(" ")
|
96
|
-
else
|
97
|
-
points.map do |point|
|
98
|
-
POINT % point
|
99
|
-
end.join(" L ").prepend("M ")
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
83
|
def params_for(categories)
|
104
84
|
params.select do |key, value|
|
105
85
|
Array(key).any? do |selector|
|
@@ -140,7 +120,8 @@ module NSWTopo
|
|
140
120
|
use.tap(&block)
|
141
121
|
|
142
122
|
category_params = params_for(categories)
|
143
|
-
font_size, stroke_width, bezier
|
123
|
+
font_size, stroke_width, bezier = category_params.values_at "font-size", "stroke-width", "bezier"
|
124
|
+
subdivide = category_params.slice("subdivide", "section").values.first
|
144
125
|
|
145
126
|
category_params.slice(*SVG_ATTRIBUTES).tap do |svg_attributes|
|
146
127
|
svg_attributes.slice(*FONT_SCALED_ATTRIBUTES).each do |key, value|
|
@@ -157,25 +138,18 @@ module NSWTopo
|
|
157
138
|
content.add_element "use", "transform" => transform, "href" => "#%s" % symbol_id
|
158
139
|
|
159
140
|
when GeoJSON::LineString
|
160
|
-
|
161
|
-
|
162
|
-
content.add_element "path", "fill" => "none", "d" => svg_path_data(linestring, bezier: bezier)
|
141
|
+
(subdivide ? feature.subdivide(subdivide) : feature).explode.each do |feature|
|
142
|
+
content.add_element "path", "fill" => "none", "d" => feature.svg_path_data(bezier: bezier)
|
163
143
|
end
|
164
144
|
|
165
145
|
when GeoJSON::Polygon
|
166
|
-
|
167
|
-
svg_path_data ring, bezier: bezier
|
168
|
-
end.each.with_object("Z").entries.join(?\s)
|
169
|
-
content.add_element "path", "fill-rule" => "nonzero", "d" => path_data
|
146
|
+
content.add_element "path", "fill-rule" => "nonzero", "d" => feature.svg_path_data
|
170
147
|
|
171
148
|
when REXML::Element
|
172
149
|
case feature.name
|
173
150
|
when "text", "textPath" then content << feature
|
174
151
|
when "path" then defs << feature
|
175
152
|
end
|
176
|
-
|
177
|
-
when Array
|
178
|
-
content.add_element "path", "fill" => "none", "d" => svg_path_data(feature + feature.take(1))
|
179
153
|
end
|
180
154
|
end if content
|
181
155
|
|
@@ -223,11 +197,12 @@ module NSWTopo
|
|
223
197
|
defs.add_element("g", "id" => symbol_id).add_element(element, attributes)
|
224
198
|
end
|
225
199
|
end
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
200
|
+
rings = features.grep(GeoJSON::Polygon).map(&:rings).flat_map(&:explode)
|
201
|
+
lines = features.grep(GeoJSON::LineString)
|
202
|
+
(rings + lines).each do |feature|
|
203
|
+
feature.sample_at(interval, offset: offset) do |point, along, angle|
|
204
|
+
"translate(%s) rotate(%s)" % [POINT, ANGLE] % [*point, 180.0 * angle / Math::PI]
|
205
|
+
end.each do |transform|
|
231
206
|
content.add_element "use", "transform" => transform, "href" => "#%s" % symbol_ids.sample
|
232
207
|
end
|
233
208
|
end
|
@@ -244,8 +219,8 @@ module NSWTopo
|
|
244
219
|
when "inpoint" then [line.first(2)]
|
245
220
|
when "outpoint" then [line.last(2).rotate]
|
246
221
|
when "endpoint" then [line.first(2), line.last(2).rotate]
|
247
|
-
end.each do |
|
248
|
-
transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*
|
222
|
+
end.each do |p0, p1|
|
223
|
+
transform = "translate(%s) rotate(%s)" % [POINT, ANGLE] % [*p0, 180.0 * (p1 - p0).angle / Math::PI]
|
249
224
|
use.add_element "use", "transform" => transform, "href" => "#%s" % symbol_id
|
250
225
|
end
|
251
226
|
end
|
@@ -264,7 +239,7 @@ module NSWTopo
|
|
264
239
|
next unless content && args
|
265
240
|
buffer = 0.5 * (Numeric === args ? args : Numeric === stroke_width ? stroke_width : 0)
|
266
241
|
features.grep_v(REXML::Element).each do |feature|
|
267
|
-
Labels::
|
242
|
+
Labels::ConvexHulls.new(feature, buffer).tap(&block)
|
268
243
|
end
|
269
244
|
|
270
245
|
when "shield"
|
data/lib/nswtopo/layer.rb
CHANGED
@@ -2,7 +2,7 @@ require_relative 'layer/raster_import'
|
|
2
2
|
require_relative 'layer/raster'
|
3
3
|
require_relative 'layer/raster_render'
|
4
4
|
require_relative 'layer/mask_render'
|
5
|
-
require_relative 'layer/
|
5
|
+
require_relative 'layer/vector_render'
|
6
6
|
require_relative 'layer/vegetation'
|
7
7
|
require_relative 'layer/import'
|
8
8
|
require_relative 'layer/arcgis_raster'
|
@@ -19,6 +19,7 @@ require_relative 'layer/labels'
|
|
19
19
|
|
20
20
|
module NSWTopo
|
21
21
|
class Layer
|
22
|
+
using Helpers
|
22
23
|
TYPES = Set[Vegetation, Import, ColourMask, ArcGISRaster, Feature, Contour, Spot, Overlay, Relief, Grid, Declination, Control, Labels]
|
23
24
|
|
24
25
|
def initialize(name, map, params)
|
data/lib/nswtopo/map.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module NSWTopo
|
2
2
|
class Map
|
3
|
+
using Helpers
|
3
4
|
include Formats, Dither, Zip, Log, Safely, TiledWebMap
|
4
5
|
|
5
6
|
def initialize(archive, neatline:, centre:, dimensions:, scale:, rotation:, layers: {})
|
@@ -15,60 +16,58 @@ module NSWTopo
|
|
15
16
|
extend Forwardable
|
16
17
|
delegate %i[write mtime read uptodate?] => :@archive
|
17
18
|
|
18
|
-
def self.init(archive, scale: 25000, rotation: 0.0, bounds: nil, coords: nil,
|
19
|
+
def self.init(archive, scale: 25000, rotation: 0.0, bounds: nil, coords: nil, neatline: nil, dimensions: nil, margins: nil, **neatline_options)
|
19
20
|
points = case
|
20
21
|
when dimensions && margins
|
21
22
|
raise "can't specify both margins and map dimensions"
|
23
|
+
when dimensions && neatline
|
24
|
+
raise "can't specify both neatline and map dimensions"
|
25
|
+
when bounds && neatline
|
26
|
+
raise "can't specify both bounds and neatline"
|
27
|
+
when coords && neatline
|
28
|
+
raise "can't specify both neatline and map coordinates"
|
22
29
|
when coords && bounds
|
23
|
-
raise "can't specify both bounds
|
30
|
+
raise "can't specify both bounds and map coordinates"
|
24
31
|
when coords
|
25
|
-
coords
|
32
|
+
GeoJSON.multipoint(coords)
|
26
33
|
when bounds
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
when gps.polygons.any?
|
31
|
-
gps.polygons.flat_map(&:coordinates).inject(&:+)
|
32
|
-
when gps.linestrings.any?
|
33
|
-
gps.linestrings.map(&:coordinates).inject(&:+)
|
34
|
-
when gps.points.any?
|
35
|
-
gps.points.map(&:coordinates)
|
36
|
-
else
|
37
|
-
raise "no features found in %s" % bounds
|
34
|
+
GPS.load(bounds).explode.tap do |gps|
|
35
|
+
margins ||= [15, 15] unless dimensions || gps.polygons.any?
|
36
|
+
raise "no features found in %s" % bounds if gps.none?
|
38
37
|
end
|
38
|
+
when neatline
|
39
|
+
neatline = GPS.load(neatline)
|
40
|
+
raise "neatline must be a single polygon" unless neatline.polygon?
|
41
|
+
neatline
|
39
42
|
else
|
40
43
|
raise "no bounds file or map coordinates specified"
|
41
|
-
end
|
42
|
-
margins ||= [0, 0]
|
44
|
+
end.dissolve_points
|
43
45
|
|
44
|
-
centre = points.
|
46
|
+
centre = *points.bbox_centre.coordinates
|
45
47
|
equidistant = Projection.azimuthal_equidistant *centre
|
48
|
+
margins ||= [0, 0]
|
46
49
|
|
47
50
|
case rotation
|
48
51
|
when "auto"
|
49
52
|
raise "can't specify both map dimensions and auto-rotation" if dimensions
|
50
|
-
local_points =
|
51
|
-
|
52
|
-
rotation *= -180.0 / Math::PI
|
53
|
+
local_points = points.reproject_to equidistant
|
54
|
+
rotation = -180 * local_points.minimum_bbox_angle(*margins) / Math::PI
|
53
55
|
when "magnetic"
|
54
|
-
rotation = declination
|
56
|
+
rotation = declination *centre
|
55
57
|
else
|
56
58
|
raise "map rotation must be between ±45°" unless rotation.abs <= 45
|
57
59
|
end
|
58
60
|
|
59
|
-
unless dimensions
|
60
|
-
local_points
|
61
|
-
|
62
|
-
|
63
|
-
end.transpose.map(&:minmax).map do |min, max|
|
64
|
-
[0.5 * (max + min), max - min]
|
65
|
-
end.transpose
|
61
|
+
unless dimensions
|
62
|
+
local_points ||= points.reproject_to equidistant
|
63
|
+
local_points.rotate_by_degrees! rotation
|
64
|
+
local_extents, local_centre = local_points.bbox_extents, local_points.bbox_centre
|
66
65
|
local_centre.rotate_by_degrees! -rotation
|
67
|
-
end
|
68
66
|
|
69
|
-
|
70
|
-
|
71
|
-
|
67
|
+
dimensions = local_extents.zip(margins).map do |extent, margin|
|
68
|
+
extent * 1000.0 / scale + 2 * margin
|
69
|
+
end
|
70
|
+
centre = *local_centre.reproject_to_wgs84.coordinates
|
72
71
|
end
|
73
72
|
|
74
73
|
params = { units: "mm", axis: "esu", k_0: 1.0 / scale, x_0: 0.0005 * dimensions[0], y_0: -0.0005 * dimensions[1] }
|
@@ -84,31 +83,47 @@ module NSWTopo
|
|
84
83
|
raise "not enough information to calculate map size – check bounds file, or specify map dimensions or margins"
|
85
84
|
end
|
86
85
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
end
|
93
|
-
collection.add_polygon [bounds.inject(&:product).values_at(0,2,3,1,0)]
|
86
|
+
if neatline
|
87
|
+
neatline = neatline.reproject_to projection
|
88
|
+
else
|
89
|
+
ring = [0, 0].zip(dimensions).inject(&:product).values_at(0,2,3,1,0)
|
90
|
+
neatline = GeoJSON.polygon [ring], projection: projection, name: "neatline"
|
94
91
|
end
|
95
92
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
93
|
+
neatline_options.each do |key, value|
|
94
|
+
case key
|
95
|
+
when :radius
|
96
|
+
radius, segments = [*value, 9]
|
97
|
+
neatline = neatline.with_sql(<<~SQL, name: "neatline").explode
|
98
|
+
SELECT ST_Buffer(ST_Buffer(ST_Buffer(geometry, #{-radius}, #{segments}), #{2*radius}, #{segments}), #{-radius}, #{segments})
|
99
|
+
FROM neatline
|
100
|
+
SQL
|
101
|
+
|
102
|
+
raise OptionParser::InvalidArgument, "radius too big" unless neatline.one?
|
103
|
+
when :inset
|
104
|
+
value.map do |corners|
|
105
|
+
corners.each_slice(2).entries.transpose.map(&:sort)
|
106
|
+
end.flat_map do |bounds|
|
107
|
+
dimensions.zip(bounds)
|
108
|
+
end.each do |dimension, (min, max)|
|
109
|
+
raise OptionParser::InvalidArgument, "inset falls outside map area" unless max > 0 && min < dimension
|
110
|
+
end
|
111
|
+
|
112
|
+
values = value.map do |corners|
|
113
|
+
%Q[(BuildMBR(#{corners.join ?,}))]
|
114
|
+
end
|
115
|
+
|
116
|
+
neatline = neatline.with_sql(<<~SQL, name: "neatline").explode
|
117
|
+
WITH insets(geometry) AS (VALUES #{values.join ?,})
|
118
|
+
SELECT ST_Difference(neatline.geometry, ST_Union(insets.geometry)) AS geometry
|
119
|
+
FROM neatline JOIN insets
|
120
|
+
SQL
|
121
|
+
|
122
|
+
raise OptionParser::InvalidArgument, "inset too big" if neatline.none?
|
123
|
+
raise OptionParser::InvalidArgument, "inset creates non-contiguous map" unless neatline.one?
|
104
124
|
end
|
105
|
-
else
|
106
|
-
ring = [[0, 0], dimensions].transpose.inject(&:product).values_at(0,2,3,1,0)
|
107
|
-
GeoJSON.polygon [ring], projection: projection, name: "neatline"
|
108
125
|
end
|
109
126
|
|
110
|
-
raise OptionParser::InvalidArgument, "inset covers map" if neatline.none?
|
111
|
-
raise OptionParser::InvalidArgument, "inset creates non-contiguous map" unless neatline.one?
|
112
127
|
new(archive, neatline: neatline, centre: centre, dimensions: dimensions, scale: scale, rotation: rotation).save
|
113
128
|
end
|
114
129
|
|
@@ -314,16 +329,15 @@ module NSWTopo
|
|
314
329
|
def info(empty: nil, json: false, proj: false, wkt: false)
|
315
330
|
case
|
316
331
|
when json
|
317
|
-
|
318
|
-
|
319
|
-
JSON.pretty_generate bbox.to_h
|
332
|
+
properties = { dimensions: @dimensions, scale: @scale, rotation: @rotation, layers: layers.map(&:name) }
|
333
|
+
JSON.pretty_generate @neatline.reproject_to_wgs84.first.with_properties(properties)
|
320
334
|
when proj
|
321
335
|
OS.gdalsrsinfo("-o", "proj4", "--single-line", @projection)
|
322
336
|
when wkt
|
323
337
|
OS.gdalsrsinfo("-o", "wkt2", @projection).gsub(/\n\n+|\A\n+/, "")
|
324
338
|
else
|
325
339
|
area_km2 = @neatline.area * (0.000001 * @scale)**2
|
326
|
-
extents_km = @dimensions.
|
340
|
+
extents_km = @dimensions.map { |dimension| dimension * 0.000001 * @scale }
|
327
341
|
StringIO.new.tap do |io|
|
328
342
|
io.puts "%-11s 1:%i" % ["scale:", @scale]
|
329
343
|
io.puts "%-11s %imm × %imm" % ["dimensions:", *@dimensions.map(&:round)]
|
data/lib/nswtopo/svg.rb
ADDED
@@ -28,7 +28,7 @@ module NSWTopo
|
|
28
28
|
png_path = yield(resolution: max_level.resolution)
|
29
29
|
end.tap do |levels|
|
30
30
|
log_update "#{extension}: creating zoom levels %s" % levels.map(&:zoom).minmax.uniq.join(?-)
|
31
|
-
end.
|
31
|
+
end.inject(ThreadPool.new, &:<<).each do |level|
|
32
32
|
OS.gdalwarp "-t_srs", "EPSG:3857", "-ts", *level.ts, "-te", *level.te, "-r", "cubic", "-dstalpha", png_path, level.tif_path
|
33
33
|
end.flat_map do |level|
|
34
34
|
cols, rows = level.indices
|
@@ -40,11 +40,11 @@ module NSWTopo
|
|
40
40
|
end
|
41
41
|
end.tap do |tiles|
|
42
42
|
log_update "#{extension}: creating %i tiles" % tiles.length
|
43
|
-
end.
|
43
|
+
end.inject(ThreadPool.new, &:<<).each do |tile|
|
44
44
|
OS.gdal_translate *tile.args
|
45
45
|
end.entries.tap do |tiles|
|
46
46
|
log_update "#{extension}: optimising %i tiles" % tiles.length
|
47
|
-
tiles.map(&:path).
|
47
|
+
tiles.map(&:path).inject(ThreadPool.new, &:<<).in_groups do |*paths|
|
48
48
|
dither *paths
|
49
49
|
rescue Dither::Missing
|
50
50
|
end
|
@@ -3,7 +3,7 @@ module NSWTopo
|
|
3
3
|
def initialize(items, parts = nil, &block)
|
4
4
|
@enum = Enumerator.new do |yielder|
|
5
5
|
next unless items
|
6
|
-
grouped =
|
6
|
+
grouped = block_given? ? block.call(items) : items
|
7
7
|
grouped.each.with_index do |(item, group), index|
|
8
8
|
*new_parts, last_part = parts
|
9
9
|
case last_part
|
@@ -15,7 +15,7 @@ module NSWTopo
|
|
15
15
|
else "├─ "
|
16
16
|
end if parts
|
17
17
|
yielder << [new_parts, item]
|
18
|
-
TreeIndenter.new(group, new_parts, &block).
|
18
|
+
TreeIndenter.new(group, new_parts, &block).each(&yielder)
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
data/lib/nswtopo/version.rb
CHANGED
data/lib/nswtopo.rb
CHANGED
@@ -19,6 +19,7 @@ require 'forwardable'
|
|
19
19
|
require 'rubygems/package'
|
20
20
|
require 'zlib'
|
21
21
|
require 'io/nonblock'
|
22
|
+
require 'bigdecimal/util'
|
22
23
|
begin
|
23
24
|
require 'hexapdf'
|
24
25
|
rescue LoadError
|
@@ -26,6 +27,7 @@ end
|
|
26
27
|
|
27
28
|
require_relative 'nswtopo/helpers'
|
28
29
|
require_relative 'nswtopo/avl_tree'
|
30
|
+
require_relative 'nswtopo/svg'
|
29
31
|
require_relative 'nswtopo/geometry'
|
30
32
|
require_relative 'nswtopo/log'
|
31
33
|
require_relative 'nswtopo/safely'
|
@@ -58,3 +60,5 @@ begin
|
|
58
60
|
require 'nswtopo/layers'
|
59
61
|
rescue LoadError
|
60
62
|
end
|
63
|
+
|
64
|
+
BigDecimal.limit 1000
|